前端综合
网站集合:
https://fe.ecool.fun/
参考:
https://juejin.cn/post/7204594495996198968
浏览器
浏览器的缓存策略(强缓存和协商缓存)?
参考: https://www.jianshu.com/p/2961ce10805ahttps://blog.csdn.net/Huangmiaomiao1/article/details/125862342https://blog.csdn.net/m0_65335111/article/details/127348516
详情请见:缓存梳理
Nodejs
process.cwd()和__dirname的区别
process.cwd(): 返回当前工作目录,比如调用node命令执行脚本时的目录
__dirname:返回源代码所在的目录
// 比如在d:\\process/index.js中
console.log(process.cwd())
console.log(__dirname)
2
3
命令 | process.cwd() | __dirname |
---|---|---|
node index.js | d:\process | d:\process |
node process\index.js | d: | d:\process |
前端综合与业务
移动端适配
<html>
<head>
<!--
设置屏幕视口配置
width=device-width:宽度等于设备屏幕的宽度
initial-scale=1.0:初始缩放比例为1,2则为放大到2倍,0.5则为缩小为原来的0.5倍
user-scalable=no:禁止用户缩放,等于下面这两个
maximum-scale=1.0:最大缩放比例为1
minimum-scale=1.0:最小缩放比例为1
-->
<meta name="viewport" content="width=device-width,initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0"/>
</head>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
前端模块化
参考:
https://juejin.cn/post/7193887403570888765
https://juejin.cn/post/6844903744518389768
https://juejin.cn/post/7166046272300777508
前端性能优化
参考:
https://juejin.cn/post/7188894691356573754
https://juejin.cn/post/7080066104613142559
https://juejin.cn/post/7326268947069534234
性能优化的方式:参考1
- html
- 减少dom数量,减轻渲染计算压力
- 异步加载/预加载/预拉取script(async无序/defer有序、preload、prefetch)、script放在最后加载
- css
- 减少css选择器复杂度,复杂度越高,浏览器解析越慢
- 避免在js中使用css表达式
- 动画使用css3过渡,减小动画复杂度
- js
- 函数防抖和函数节流:防抖(输入框输入,按钮点击避免重复请求,接口查询),节流(滚动条滚动、dom拖拽)
- web worker优化长任务:web worker是浏览器中多线程技术,可以将一些耗时长的操作交给web worker处理,主线程只负责渲染页面,避免页面阻塞。
- 使用requestAnimation代替setTimeout、setInterval:requestAnimationFrame是浏览器提供的API,它是由浏览器专门为动画提供的API,使用该方法将由浏览器自动优化动画过程,减少卡顿。
- 动态导入
import('xxx')
:按需(符合条件才)加载,避免一次性加载所有资源。应用场景有:路由懒加载、组件动态导入等
- 构建工具
- 代码分割(分包策略):避免将所有代码打包到一个文件中,减少文件体积。
- 按需加载:按需加载,比如路由懒加载。
- 代码压缩
- 静态资源优化:字体图标、svg、图片转base64,使用cdn加载
- tree shaking:删除未使用的代码,减少打包体积
- 网络
- 开启前端缓存
- 开启Gzip压缩:减小打包文件的体积,提升加载速度
- 合并、减少http请求:合并css、js
- 综合
- 组件按需引入:打包时将删掉未使用的组件,减少打包体积
- 长列表虚拟滚动:只渲染可视区域内的数据,减少DOM节点数量,提升性能,使用插件
- 路由懒加载:不使用路由懒加载时,单页面项目中第一次打开会一次性加载所有的资源,造成首屏加载慢,用户体验差。webpack在路由导入时通过webpackChunkName(注释的形式)设置分割后代码块的名字
() => import(/* webpackChunkName: "xxx" */ "../views/xxx/index.vue")
,vite不需要直接导入即可() => import('xxx')
- 使用字体图标代替图片:字体图标是矢量图,体积小,加载快、兼容性好
- 使用懒加载技术:在需要的时候才加载,减少不必要的内存占用和页面负载,使用插件(比如图片懒加载,将图片地址放在data-xxx上,待图片出现在可视区域(绑定scroll监听),设置到src上)
- 使用骨架屏插件:在页面加载的时候,先展示一个骨架屏,等数据加载完成,再展示真实的内容
- 使用服务端渲染:加快首屏渲染速度
- 减少重绘和重排:现代项目都采用虚拟dom,无需太过于关注
开启前端缓存的方式:
- 构建工具打包优化,生成文件hash
- html文件引入meta标签,设置缓存
- 服务器(比如nginx)设置缓存
开启前端缓存的方式
1: webpack打包文件hash,设置文件hash的目的是方便缓存设置,有文件hash的情况下,当文件名未发生变更之前,只要设置的缓存时间未到,浏览器就会直接使用缓存。而当无文件hash时,新内容构建后,可能会命中强制缓存策略,导致获取不到最新的内容(此处也可加强制缓存+协商缓存避免)。
// webpack.config.js
const webpack = require('webpack')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const commonConfig = {
entry: './src/index.js',
mode: 'none',
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
]
}
]
}
}
function generateConfig() {
return webpack([
// fullhash:生成的所有构建产物都使用相同的hash
// 意味着修改任何一个文件,都会导致所有文件的hash发生改变
{
...commonConfig,
output: {
filename: '[name].[fullhash].js'
},
plugins: [
new MiniCssExtractPlugin({
// css单独输出文件
filename: '[name].[fullhash].css',
path: path.resolve(__dirname, 'dist/fullhash'),
clean: true,
})
]
},
// chunkhash:同一个chunk下的文件使用相同的hash
// 意味着修改同一个chunk中的文件,只会导致同属该chunk文件的hash发生改变
// 同一个chunk:比如导入文件import(/* webpackChunkName: "chunk-A" */ './index.js')中,若名字都是chunk-A,则属于同一个chunk,同时文件内部使用的其他文件也会被打包到该chunk中
{
...commonConfig,
output: {
filename: '[name].[chunkhash].js',
path: path.resolve(__dirname, 'dist/chunkhash'),
clean: true,
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[chunkhash].css',
})
]
},
// contenthash:每个文件使用不同的hash,互不干扰
{
...commonConfig,
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist/contenthash'),
clean: true,
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css',
})
]
}
])
}
f2().run(() => {
console.log('build success')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
2: vite打包文件hash,目前vite打包每次会生成不同的hash,解决方案如下:
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
chunkFileNames: 'static/chunk/[name].[hash].js',
entryFileNames: 'static/entry/[name].[hash].js',
assetFileNames: 'static/assets/[name].[hash].[ext]'
}
}
}
})
2
3
4
5
6
7
8
9
10
11
12
3: 在html中设置meta标签,让所有的资源重新加载,mdn和html标准中未提及,详细请见这里
<html>
<head>
<!-- http-equiv在mdn标准中并无下述属性 -->
<meta http-equiv="cache-control" content="no-cache, no-store, must-revalidate"/>
<meta http-equiv="pragma" content="no-cache"/>
</html>
2
3
4
5
6
http {
# 项目1
server {
listen 80;
server_name localhost;
root /home/www;
location ~ .*\.(?:jpg|jpeg|png|gif)$ {
expires 7d;
}
location ~ .*\.(js|css)$ {
expires 7d;
}
# 不缓存html文件
location ~ .*\.(html)$ {
add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
}
}
# 项目2
# 缓存目录/home/www/cache、缓存目录级别2
# 缓存名称my_cache、缓存占用内存时间10m
# 缓存最大时间60s、缓存硬盘空间5g
proxy_cache_path /home/www/cache levels=1:2 keys_zone=my_cache:10m inactive=60s max_size=5g;
server {
listen 82;
# 使用名称为my_cache的缓存
proxy_cache my_cache;
location / {
# 使用默认的缓存配置
proxy_pass http://127.0.0.1:80;
}
location /some/path {
proxy_pass http://127.0.0.1:80;
# 状态200 304的缓存有效期,状态可以是any
proxy_cache_valid 200 304 1h;
# 被请求三次以上才缓存
proxy_cache_min_uses 3;
# 请求时有下面参数不走缓存
proxy_cache_bypass $cookie_nocache $arg_nocache$arg_comment;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
开启Gzip压缩:
// vue.config.js
const CompressionWebpackPlugin = require('compression-webpack-plugin')
module.exports = {
configureWebpack: config => {
if (process.env.NODE_ENV !== 'production') {
return
}
return {
plugins: [
new CompressionWebpackPlugin({
filename: '[path].gz[query]',
algorithm: 'gzip',
test: /\.js$|\.css$|\.html$|\.ttf$|\.woff$/,
// 超过10kb的才压缩
threshold: 10240,
// 最小压缩率
minRatio: 0.8,
// 是否删除原文件
deleteOriginalAssets: false
})
]
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// vite.config.js
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
viteCompression({
filter: /\.(js|css|html|svg|ico|json)$/i,
threshold: 1024,
algorith: 'gzip',
ext: 'gz',
deleteOriginFile: false
})
]
})
2
3
4
5
6
7
8
9
10
11
12
13
http {
server {
# ...
gzip on;
gzip_min_length 4k;
# 处理请求压缩的缓冲区数量和大小
gzip_buffers 4 16k;
# 压缩级别,1-9,数字越大压缩的越好,也越占用CPU时间
gzip_comp_level 2;
# 压缩类型,默认text/html
gzip_types text/plain application/x-javascript text/css application/xml;
# 是否在http header中添加Vary: Accept-Encoding,建议开启
gzip_vary on;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
函数防抖和节流:
- 函数防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时(时间内触发最后一次),
- 函数节流:在单位时间内只能触发一次事件,如果在n秒内再次触发,则忽略(时间内仅触发第一次),
应用场景:
- 防抖:减少频繁触发,确保只有最后一次操作得到执行,输入框实时搜索、窗口resize等
- 节流:控制高频事件的触发频率,比如scroll、按钮点击避免多次提交等
// 防抖函数
function debounce(fn, delay) {
let timer = null
return function () {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}
const handleInput = debounce(function () {
consol.log('防抖函数')
}, 1000)
document.querSelector('#input').addEventListener('keyup', handleInput)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 节流函数
function throttle(fn, delay) {
let flag = true
return function () {
if (!flag) {
return
}
flag = false
setTimeout(() => {
fn.apply(this, arguments)
flag = true
}, delay)
}
}
const handleScroll = throttle(function () {
cnsol.log('节流函数')
}, 1000)
window.addEventListener('scroll', handleScroll)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
token刷新
解决方案:
- 后端返回过期时间,前端判断token过期时间,去调用刷新token接口
- 后端拦截请求,验证token是否过期,过期则重新生成token将其放在响应头中,前端在响应拦截器中判断是否有无新token,有则进行替换(请求后)
- 使用定时器定时刷新token接口
- 在请求拦截器(比如
axios.interceptors.request.use
)中拦截,判断token是否将要过期,调用刷新token接口(请求前)
关键词解析:
- 认证(authentication):验证当前用户的身份
- 授权(authorization):用户授予第三方应用访问该用户某些资源的权限
- 凭证(credentials):实现认证和授权的前提是需要一种媒介(证书)来标记访问者的身份,而凭证就是这个媒介
- JWT(JSON Web Token):跨域认证解决方案,一种认证授权机制
- access token:访问资源接口时所需要的资源凭证
- refresh token:用于刷新access token的token
- cookie:本地存储,会在浏览器下次向同一服务器再次发起请求时被携带并发送到服务器上,顶级域名下共享cookie
Tree-shaking
参考:
https://juejin.cn/post/7109296712526594085
https://juejin.cn/post/7002410645316436004
dead code定义:
- 代码不会被执行,代码不可到达(它所处的位置)
- 代码执行的结果不会被用到
- 代码只会影响死变量(只写不读的变量,即该变量不会对其他内容产生影响)
定义:
- tree shaking是一种基于es module规范的dead code elimination技术,它会在运行过程中静态分析模块之间的导入导出,确定ESM模块中哪些导出值未被其他模块使用,并将其删除,以此来实现打包产物的优化
- tree shaking是一个通常用于描述移除JavaScript上下文中未引用代码dead code行为的术语
作用:
- 减少最终的构建体积
在webpack中启动tree shaking的三个条件:
- 使用ESM编写代码
- 配置
optimization.usedExports = true
,启动标记功能 - 启动代码优化功能,有下列三种方式:
- 配置
mode = production
- 配置
optimization.minimize = true
- 提供
optimization.minimizer
数组
- 配置
// webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true
}
}
2
3
4
5
6
7
关闭tree shaking:
- 在package.json中设置一级字段
sideEffects: false
,表示所有代码都不包含副作用,这时所有代码都会按照插件默认规则进行tree shaking - 在package.json中设置一级字段
sideEffects
为一个数组,数组值表示会产生副作用的文件,此时数组内匹配到的文件将不会被tree shaking - 在webpack.config.js的rules字段中对应规则下设置sideEffects字段选择是否进行tree shaking
// package.json
{
// 所有的代码都不会被tree shaking
"sideEffects": true,
// 数组内匹配到的文件不会被tree shaking
"sideEffects": ["./src/app.js", "*.css"]
}
2
3
4
5
6
7
module.exports = {
module: {
rules: [
{
test: /\.jsx?$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader'
},
sideEffects: false || []
}
]
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
tree shaking最佳实践:
- 避免无意义的赋值或引用,即被赋值的变量后续未被使用到
- 在调用前使用
/*#__PURE__*/
备注,告诉webpack该次函数调用不会对上下文环境产生副作用,可以进行tree shaking - 禁止Babel转译模块的导入导出语句,因为会导致webpack无法对转义后的模块导入导出内容做静态分析,使得tree shaking失效
- 优化导出值的粒度,在exports中,不能进行赋值/初始化操作,应该初始化赋值完毕后,再用exports将该变量导出
- 使用支持tree shaking的包,比如使用lodash-es替代lodash
虚拟dom和真实dom的区别
背景:
- DOM的缺陷:dom节点的操作会影响到渲染流程,同时Dom节点的增删改都会触发样式计算、布局、绘制等任务(重排的过程),同时还会触发重绘
- 一个复杂的页面中,重排和重绘非常消耗资源,也非常费时
虚拟DOM做的事情:
- 将页面改变的内容应用到虚拟DOM(一个JS对象)上
- 调整虚拟DOM的内部状态
- 虚拟DOM收集到足够多改变时,再将变化一次性应用到真实的DOM上(也是调用dom操作的一步,若直接操作真实dom,就没有前面2步,会发生频繁的重排重绘操作)
组件封装的一些技巧
- 插槽的用法:组件封装时,需要考虑到通用性。比如一个dialog组件,需要显示中间的信息和底部的操作按钮。此时需要考虑的场景是当组件使用时,是否需要中间/底部栏,则在封装组件的时候,通过
v-if='$slot.name'
判断用户是否使用了相应的中间/底部栏,否则会一直展示中间/底部栏的位置。 - 组件封装时,组件通信的技巧:通过vue语法糖
.sync
来进行组件间的传值,用法如下:使用该组件时::data.sync="sync"
,自定义组件内部通过某些点击事件来传送该值到父组件中:this.$emit('update:data', value)
- 自定义组件注册:在使用
Vue.component()
注册组件时,第一个参数可以使用组件的名字(component.name)
,此处的name即exports里面的name - 组件注册自定义属性的时候,在自定义组件内部使用props时,需验证属性的类型,以及默认值等信息。同时在使用该组件时,不一定必须使用
v-bind
对属性进行绑定,因为默认情况下是字符串,同时对于一些显示隐藏的属性,可根据是否有值来判定/设置相应的class。 - 输入框组件封装重点:
v-model
的使用技巧。使用组件时:v-model="value"
,自定义组件内部::value="value" @input="handleInput" handleInput (e) { this.$emit('input', e.target.value) }
,这样才能实现数据的双绑定。 - 输入框组件密码显隐:展示密码和展示右侧的图标。密码的显隐切换是由type类型来控制的,故当传入密码显示属性
password
时,type应进行判断,有password则通过password来判断是否在password和text之间切换,无password属性则直接为password。控制password的变化需要在自定义组件内部加一个passwordVisible属性来切换
主题换肤
方案1:维护多套css,主要方案有:
- 通过切换link的href来切换css文件(缺点:网络不佳时切换不流畅、样式优先级问题、仅针对预设主题)
- 通过切换根节点class(缺点:样式优先级问题、延长首屏加载时间、仅针对预设主题)
切换link的href示例
const link = document.getElementsByTagName('link')[0]
link.href = 'dark.css'
2
切换根节点class示例
css代码:
body.dark {
color: #000;
}
body.light {
color: #eee;
}
2
3
4
5
6
7
js代码:
const html = document.getElementsByTagName('html')[0]
html.className = 'dark'
2
方案2:维护多套css变量,主要方案有:
- (css文件)全局维护多套css变量(比如:默认
:root { --theme-color: #333; }
、黑暗模式.dark{ --theme-color: #000; }
、明亮模式.light { --theme-color: #eee; }
),然后在各个地方正常使用这些变量,通过切换根节点class或给根节点设置不同的data-*
属性来切换不同的模式(缺点:延长首屏加载时间) - (scss文件)全局维护多套scss变量,方式同上(缺点:延长首屏加载时间)
- (js文件)全局维护多套css变量对象,可以封装成一个类或者hook,将主题变量对象赋值给hook的某个变量,然后进行主题的切换修改这个变量的值。其他地方正常使用,对于vue3,可通过v-bind绑定css变量(缺点:延长首屏加载时间、v-bind的性能问题)
全局维护多套css变量的示例
/* 第一种:根据类名 */
:root {
--theme-color: #333;
}
.dark {
--theme-color: #000;
}
.light {
--theme-color: #eee;
}
.my-btn {
color: var(--theme-color);
}
/* 第二种:根据属性 */
$theme-color: #333;
$dark-theme-color: #000;
$light-theme-color: #eee;
@mixin theme_color {
color: $theme-color;
[data-theme='dark'] & {
color: $dark-theme-color;
}
[data-theme='light'] & {
color: $light-theme-color;
}
}
.my-btn {
@include theme_color;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 根据class
docuemnt.getElementsByTagName('html')[0].className = 'dark'
// 根据data-*
document.getElementsByTagName('html')[0].setAttribute('data-theme', 'dark')
2
3
4
5
全局维护多套css变量对象的示例
// 定义hook
const theme = shallowRef({})
export const lightTheme = {
themecolor: '#eee'
}
export const darkTheme = {
thmecolor: '#000'
}
export function useTheme () {
const localTheme = localStorage.getItem('theme')
theme.value = localTheme ? JSON.parse(localTheme) : lightTheme
const setLightTheme = () => {
theme.value = lightTheme
}
const setDarkTheme = () => {
theme.value = darkTheme
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- 使用hook -->
<script setup>
import { useTheme } from './theme.js'
const { theme, setLightTheme, setDarkTheme } = useTheme()
</script>
<template>
<div class="box">
hello, jade!
</div>
<button @click="setLightTheme">light</button>
<button @click="setDarkTheme">dark</button>
</template>
<style lang="scss" scoped>
.box {
color: v-bind(theme.themecolor);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
方案3:维护一套css变量,自定义颜色主题,主要方案有:
- 维护一套css,通过修改某个变量来修改对应的一组主题颜色,比如动态修改主题颜色为red,则需要将所有相关的css变量(使用
dom.style.setProperty(prop, val)
)都改为red
维护一套css变量的示例
维护基本的css变量:
:root {
--theme-color: #333;
--theme-bg: #eee;
}
2
3
4
修改css变量,比如修改主题色:
const setCssVar = (prop, val, dom = document.documentElement) => {
dom.style.setProperty(prop, val)
}
// 一般是一组该操作
setCssVar('--theme-color', '#000')
setCssVar('--theme-bg', '#000')
// ......
2
3
4
5
6
7
8
方案4:使用框架自带的主题配置,比如:
- element
- 原子类框架:tailwindcss等
使用tailwindcss框架自带的主题配置示例
定义全局的css变量:
@tailwind base;
@tailwind components;
@tailwind utilities;
.theme-noraml {
--theme-color: #333;
}
.theme-dark {
--theme-color: #000;
}
.theme-light {
--theme-color: #eee;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
配置tailwind.config.js
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {
// 使用class: bg-primary/[0.5],透明度为0.5的bg-primary
backgroundColor: {
primary: 'rgb(var(--theme-color) / <alpha-value>)',
},
// 使用class: text-primary
textColor: {
primary: 'rgb(var(--theme-color) / <alpha-value>)',
}
}
},
darkMode: 'class',
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用:
<script setup>
// 这里的theme,可配合持久化使用(比如缓存到localstorage)、封装成hooks
const theme = ref('light')
const changeTheme = (val) => {
theme.value = val
}
</script>
<template>
<!-- 定义class,当class改变时,内部所有的变量都会跟着修改 -->
<div :class="[theme]">
<!-- 使用tailwind定义的类 -->
<p class="text-primary/[0.5] bg-primary">
hello, jade!
</p>
</div>
<button @click="changeTheme('dark')">dark</button>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
防止重复提交
文件上传
文件上传用户操作方式:
- 点击上传
- 拖拽上传
文件上传方式:
- 原生JavaScript表单上传
- XMLHttpRequest(Ajax、axios):数据体是formdata
- fetch:数据体是formdata
- 第三方库:vue-simple-uploader等
- 云存储服务
文件上传的步骤:
- 读取文件
- 获取文件信息,生成(md5库:比如spark-md5)唯一hash
- 文件分片
- 根据has和文件名获取文件是否已上传(全部上传、部分上传:获取已上传的分片名列表)
- 上传未上传的分片(formdata信息:分片名、分片数据、文件hash)
文件上传优化策略:
- 断点续传(系统崩溃或者网络中断等异常因素导致上传中断,需要记录上传的进度,后续再次上传)
- 秒传
- 使用web worker优化主线程I/O流阻塞(文件操作、MD5计算慢)的问题
- hash生成优化:比如使用抽样方式计算MD5,将文件分成若干切片,每个切片选取固定字节数进行MD5计算,然后合并成最终的hash,速度快,不过精度会降低
- 请求并发数量控制
- 实时进度条监听(比如axios的onUploadProgress方法)
- 上传失败重试(控制重试次数)
- 多文件上传
- 文件夹上传(input的webkitdirectory属性),文件或文件夹二选一,支持多种形式,需要有读取系统文件的能力(比如electron构建的app)
- 上传前验证(格式、大小)
使用到的技术:
- Blob:https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
- Formdata:https://developer.mozilla.org/zh-CN/docs/Web/API/FormData
- FileReader:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader
- ArrayBuffer:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer
- input-file:https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input/file#非标准属性
- Web_Workers:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API
文件下载
文件下载方式:
- window.open:新标签页,可下载大文件,打开一个无内容的窗口,完成下载后自动关闭,可监听该窗口是否关闭,判断是否下载完成
- location.href:新标签页
- a标签+download属性
- blob/base64(文件流):将数据存储到内存,然后资源转换后再进行下载。当资源很大时,浏览器分配到的内存是有限的,过多的占用内存会导致卡顿崩溃
- 分片下载:每个下载的文件块(chunk)在客户端进行缓存/存储(IndexedDB、storage等),方便实现断点续传,以及后续将这些文件块合并成完整的文件,一般情况下,为了避免占用过多的内存,推荐将文件块暂时保存在客户端的本地存储中,确保在下载大文件时不会因为内存占用过多而导致性能问题
- stream api(ReadableStream),也可结合service worker一起使用,在较新的浏览器中支持
- 第三方库:FileSaver(支持2gb)、StreamSaver(支持2gb+)
使用到的技术:
- Content-Disposition:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
- ReadableStream:https://developer.mozilla.org/zh-CN/docs/Web/API/ReadableStream
- service worker:https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers
- IndexedDB:https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API,封装库有localForage、dexie.js、PouchDB、idb、idb-keyval、JsStore等
参考:
实时数据更新
实时数据更新方式:
- 短轮询(polling)
- 长轮询(long polling)
- 长连接(websocket)
- 服务器事件推送(server-sent events, SSE)
短轮询:客户端不停地调用服务端接口获取最新的数据,发起请求后服务端会立即响应并返回结果给客户端,客户端在接受到数据后,(可能等待几秒后)会再次发起请求,如此反复。
优点:
- 实现简单
缺点:
- 无用的请求多,因为客户端不知道服务端什么时候有数据更新,所以只能不停的询问服务端。这将增加服务端带宽,消耗服务端资源,同时也会加快客户端的耗电速度。
- 数据实时性差:在获取到服务端数据后,可能会等待一些时间客户端才开始再次发起请求,这将导致客户端需要一段时间才能拿到最新的数据。对于实时性要求高的场景是致命的。
长轮询:客户端发起请求后,服务端发现当前没新的数据,这是不会立即返回请求,而是将请求挂起,直到有新的数据更新,或者等待超时(比如设置的30s)后再将内容发给客户端。客户端在收到数据后,立即发起新的请求,如此反复。
优点:
- 避免客户端大量的重复请求,因为服务端在数据未更新时不会立即将结果返回给客户端
- 客户端在收到数据后,可以立即发起新的请求,确保数据实时性
缺点:
- 大量消耗服务端资源,服务端会一直hold客户端的请求,这些请求会占用服务器的内存资源,因为每个http请求都是一个独立的连接,当请求数量增多时,服务器的内存资源很快就会被耗光
- 难以处理数据更新频繁的情况,频繁更新数据会创建和重建大量的连接,导致服务端资源消耗过多
websocket:首先客户端会给服务端发送一个http请求,这个请求的header会告诉服务端它想基于websocket协议通信,如果服务端支持升级协议,会给客户端发送一个switching protocol的响应,后续都是基于websocket协议进行通信(客户端和服务端之间建立一个持久的长连接,这个连接是全双工的,客户端和服务端都可以主动实时地发送数据给对方)。
打开开发者工具,选择Network,然后刷新页面,可以看到一个ws的请求,在messages这栏可以看到客户端和服务端通信的数据。
优点:
- 客户端和服务端建立连接的次数减少:理想情况下客户端只需要发送一个http升级协议就可以升级到websocket连接,后续所有的消息都是通过这个通道进行通信,无需再次建立连接
- 消息实时性高:由于连接是一直建立的,当有数据更新时可以马上推送到客户端
- 双工通信:客户端和服务端都可以主动发送数据给对方
- 适用于数据频繁更新的场景:随时推送,无需建立重连接
缺点:
- 扩容麻烦:基于websocket的服务都是有状态的,意味着在扩容的时候麻烦,系统设计也比较复杂
- 代理限制:某些代理服务器(比如nginx)默认配置的长连接时间是有限的(比如几十秒),这时需要自动重连,突破这种现在需要将所有的代理默认配置都进行更改。
服务端事件推送:是一种基于http协议的实时向客户端进行数据推送(单向)的技术。首先客户端向服务端发起一个持久化的http连接,服务端收到请求后,挂起客户端请求,有数据更新时,再通过这个连接将数据推送给客户端。
打开开发者工具,选择Network,然后刷新页面,可以看到一个http的请求,在eventStream这栏可以看到服务端推送的消息。
优点:
- 连接数少:只有一个持久化的http连接
- 数据实时性高:服务端和客户端的连接都是持久的,当有数据更新时,服务端可以立即推送给客户端
缺点:
- 单向通信:客户端无法主动向服务端发送数据
- 代理层限制:代理服务器(比如nginx)默认配置的长连接时间有限,需要自动重连
const fetchLatestEvents = async (timestamp) => {
// 获取最新事件
const body = await fetch(`http://xxx/events?timestamp=${timestamp}`)
if (body.ok) {
return await body.json()
} else {
console.error('获取最新事件失败')
}
}
// 每3000秒获取一次最新事件
setInterval(async () => {
const latestEvents = await fetchLatestEvents(lastTimestamp)
if (latestEvents && latestEvents.length) {
// 更新数据
data = [...data, ...latestEvents]
}
}, 3000)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const fetchLatestEvents = async (timestamp) => {
const body = await fetch(`http://xxx/events?timestamp=${timestamp}`)
if (body.ok) {
return await body.json()
} else {
console.error('获取最新事件失败')
}
}
const fetchTask = async () => {
const latestEvents = await fetchLatestEvents(lastTimestamp)
if (latestEvents && latestEvents.length) {
// 更新数据
data = [...data, ...latestEvents]
}
}
fetchTask().catch(console.error).finally(() => {
// 触发下一次请求
fetchTask()
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ws = new WebSocket('ws://xxx/events?timestamp=1645890723')
ws.addEventListener('open', () => {
console.log('连接成功')
})
ws.addEventListener('message', (event) => {
const latestEvents = JSON.parse(event.data)
if (latestEvents && latestEvents.length) {
// 更新数据
data = [...data, ...latestEvents]
}
})
ws.addEventListener('close', () => {
console.log('连接关闭')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const source = new EventSource('http://xxx/events?timestamp=1645890723')
source.onopen = () => {
console.log('连接成功')
}
source.onmessage = (event) => {
const latestEvents = JSON.parse(event.data)
if (latestEvents && latestEvents.length) {
// 更新数据
data = [...data, ...latestEvents]
}
}
source.addEventListner('error', (e) => {
consoe.log('连接失败')
// 关闭连接,或者做其他操作
source.close()
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
大屏定时刷新内存泄露问题
场景:项目使用 ECharts 绘制图形时,需要定时刷新数据,数据量太大时,页面长时间停留后会卡顿
解决思路:
- 不要把 ECharts 实例或者配置项挂到 vue 下面,Vue 会深度遍历监听所有属性,数据量一大就卡了
- 组件卸载时清除定时器
- 保证页面中同一个图表只有一个实例,而不是每次刷新数据都新建 ECharts 实例,数据更新只更新option
其他
遇到的难解决的问题
难解决的问题分为两种:
- 业务问题,需求不清。这时需要拉上懂业务的同事理清需求,必要的时候需要调整设计。同时自主学习,增强对业务的了解。
- 技术问题,可能是由于之前技术栈限制导致需求难以实现,或者说现有技术导致实现需求会有性能、可维护性问题,或者是自身储备或者周边资源不足(比如说没有现成的组件库)导致工期比预想长。可以通过最小限度实现需求、请教公司或同项目组的同事寻找合适的工具、交叉集成其他框架等方式解决,但最重要的是及早沟通。
注意:
- 遵循star法则回答