组件之间的传值
- props(父传子)
- emit(子传父)
- expose/ref :子组件通过 expose 暴露自身的方法和数据,父组件通过 ref 获取到子组件
- attrs
- v-model:v-model 是 Vue 的一个语法糖。在 Vue 3 中的玩法就更多
- 插槽slot
- provide / inject(依赖注入多层传值)
- Vuex/pinia
- mitt
js 数组的方法
- 增 下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响
push() unshift() splice() concat()
-
删除 pop() shift() splice() slice()
-
改 即修改原来数组的内容,常用splice
-
查 即查找元素,返回元素坐标或者元素值
indexOf() includes() find()
-
排序 reverse() sort()
-
转换 join()
-
迭代 some() every() forEach() filter() map()
4.vue2的响应式数据原理?
1.首先对于数据可以通过Observer把对象的属性都变成响应式对象,Observe是通过Object.defineProperty将对象转化为带有getter和setter的属性,它会递归遍历对象的所有属性,以完成深度的属性转换。
由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此vue提供了delete两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。 而对于数组是这样处理的,首先进来判断当前是不是一个数组对象,如果是一个数组对象,则开始如下的操作,先产生一个代理对象,const proxyPrototype = Object.create(arrayPrototype) array.prototype 产生的一个对象,然后将传进来的数组对象的原型设置为这个代理对象,最后通过定义Object.defineProperty重新定义数组的七个方法,然后再七个方法里面,先执行array.prototype 的方法,然后再执行派发更新的操作。
// Observer.js
if (Array.isArray(value)) {
Object.setPrototypeOf(value, proxyPrototype) // value.__proto__ === proxyPrototype
this.observeArray(value)
}
// array.js
const arrayPrototype = Array.prototype // 缓存真实原型
// 需要处理的方法
const reactiveMethods = [
'push',
'pop',
'unshift',
'shift',
'splice',
'reverse',
'sort'
]
// 增加代理原型 proxyPrototype.__proto__ === arrayProrotype
const proxyPrototype = Object.create(arrayPrototype)
// 定义响应式方法
reactiveMethods.forEach((method) => {
const originalMethod = arrayPrototype[method]
// 在代理原型上定义变异响应式方法
Object.defineProperty(proxyPrototype, method, {
value: function reactiveMethod(...args) {
const result = originalMethod.apply(this, args) // 执行默认原型的方法
// ...派发更新...
return result
},
enumerable: false,
writable: true,
configurable: true
})
})
由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性,因此vue提供了delete两个实例方法,让开发者通过这两个实例方法对已有响应式对象添加或删除属性。因为这个原因vue3采用proxy来处理。
Vue会为响应式对象中的每个属性、对象本身、数组本身创建一个Dep实例,每个Dep实例都有能力做以下两件事:
- 记录依赖:是谁在用我
- 派发更新:我变了,我要通知那些用到我的人
执行该函数的更新组件render函数的时候,会创建一个watcher,watcher里面把更新组件render函数穿进去,然后在watcher里面把更新组件render函数执行一遍,执行的时候读取到响应式数据,这时就会调用前面的响应式数据的get方法,然后响应数据就把这个watcher放到Dep里面,这就是收集了依赖。
如果后面需要响应式数据改变了,这时就会触发set方法,然后来把更新dep里面的watcher去重新执行组件的更新组件render方法,由于可能会遇到一个组件的两个响应式数据都改变,如果直接执行就会执行两次。所以vue里面的处理是通过将watcher内部收到的派发更新去交给一个调度器的东西去处理。
调度器维护一个执行队列,该队列同一个watcher仅会存在一次,队列中的watcher不是立即执行,它会通过一个叫做nextTick的工具方法,把这些需要执行的watcher放入到事件循环的微队列中,nextTick的具体做法是通过Promise完成的。
由于我们是异步执行的,所以我们要使用nextTick()的回调函数去拿dom的信息,如果不这样,可能拿到的数据还是修改前的。
vue diff 算法
在组件更新时,vue内部会使用render函数生成虚拟dom树,然后将新旧两树进行对比,找到差异点,最后更新到真实dom,对比差异所采用的算法叫diff算法。 vue采用深度递归、同层比较的方式进行比对。
Vue2的diff算法内部采用双指针的方式,它依次通过头头,尾尾,头尾,尾头的方式进行比较,在源码中判断两个节点是否是相同节点,是通过Key(vue里面设置的key)和tag(标签类型)来判断的,但是对于input还要看他们的type属性是不是一样的。
如果是相同节点,则进入更新流程,并且把对应的指针往中间移动。更新流程主要是将对应的真实dom指向新的虚拟节点,更新真实dom的属性,处理当前节点的子节点,递归进行上面的操作。
如果经过上面的步骤,新节点中还有没处理的,则根据老节点列表生成一个key和位置的映射表,然后依次取出新节点的key,去映射表中找。如果找到了,则进入更新流程,如果新节点中在映射表找不到的,则需要新增真实dom。最后老节点里面没有用到的节点,则需要删除。
而Vue3采用的是快速地diff算法,通过双端对比的方式,先通过头头和尾尾对比找出可复用的节点。如果头尾对比结束,新节点列表中还有剩余节点,那么就建立一个数组,用来记录剩余新节点在老节点列表的位置,计算这个数组里面最长递增子序列,然后将最长递增序列中的节点位置保持不动,移动其他节点位置,来达到最少移动。 其实vue3的时间复杂度比vue2的还高,但是vue3减少dom的移动,因为dom的移动比js操作更消耗性能,所以还是利大于弊。
~~一个新旧节点位置映射表,用来记录剩余新节点在老节点列表中的位置。如果新节点在映射表中不到,可代表该新节点需要新建真实dom。而可以找到则表示可以复用,然后在根据新旧节点位置映射表中计算最长递增子序列,然后将最长递增序列中的节点位置保持不动,移动其他节点位置,来达到最少移动。 其实vue3的时间复杂度比vue2的还高,但是vue3减少dom的移动,因为dom的移动比js操作更消耗性能,所以还是利大于弊。 ~~
1.移动端debug 模式
移动端的调试可以使用
- vConsole 引入比较简单,内置于项目,打印移动端日志,查看网络请求以及查看 Cookie 和 Storage
- 真机调试 Mac+IOS+Safari- 进阶调试 Chrome+Android- 进阶调试 前面两种需要连线,而后面spy-debugger- 进阶调试只需要手机和pc在同一个网络就可以了
Chrome 中输入:chrome://inspect,进入调试页面。
-
Charles 青花瓷 这个我做iOS做的开发比较多,主要用于iOS的网络请求抓包,他有一个作用可以修改响应的结果,你也可以把响应的文件映射改为本地,这在调试线上的问题的有时会比较有用。 Fiddler 适合 Windows 平台,与 Charles 类似,查看、控制网络请求,分析数据情况
-
whistle(weiso) 是基于 Node 实现跨平台抓包工具,他可以实现抓包重放,修改请求,修改响应。查看修稿dom结构,执行代码等。
我在实际开发中其实vConsole 和whistle 用的比较多,只要真机出问题采用真机调试以下,Charles 青花瓷 这个我做iOS做的开发比较多,做前端开发后用的也不多。
1.5 webview性能优化
- 性能优化方面:
除此之外,因为我懂客户端这块,我也做了iOS客户端的方面的优化 从打开webview的界面到显示内容经历了两个大的过程,第一个过程是原生webview组件的加载, 第二个过程是H5页面加载资源,渲染的时间。 所以工作的时候做过一段时间的iOS 性能监控的SDK监控数据,我们统计的webview的打开时间就会将webview的打开时间也加入,单纯依靠web 提高的 window.performance.timing的数据是不准确。 这里的第一个优化就是客户端那里,在app打开初始化一个全局的webview,后来去打开webview的时候就可以节省调前面的时间,对于iOS来说第二次打开webview 要比第一次打开快 700ms左右。
第二段时间就是前端常规的性能优化方式
降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
渲染:JS/CSS优化,加载顺序,服务端渲染模板直出。
如果前面的优化体验还不够,对于那些样式,功能不怎么变化的界面,那可以在客户端那里进行下载离线资源包,可以把css,部分js,存到客户端,包括里面的图片资源,也可以通过客户端比方说拦截这些网络请求,通过图片下载框架,把图片缓存到本地。 然后页面打开后只去下载会变化的数据,进一步优化,基本经过这个办法,就可以达到最好的效果。
2.虚拟滚动
这里的背景是这样的, 我们的在线上比赛的时候是可以通过音乐列表去选择录制视频,这里的音乐列表可能比较长,滚动的时间长了,里面加载的dom元素可能比较多,滑动比较卡顿的问题,使用了虚拟滚动。
音乐列表的这里每一项的高度是固定高度,做法的原理其实是有一个父组件,父组件里面有一个container组件,然后这个可以根据container 的高度可以根据里面的item 组件高度和加载的个数确定,然后滚动的时候呢,根据 container 的scrolltop,然后计算当前显示区域之前应该从哪一个开始显示,最下面的那个显示区域显示哪一个,然后里面的item可以通过设置定位,然后通过设置 transform: ranslateY(${poolItem.position}px),
然后把这些数据通过作用域插槽传给父组件里面的去加载这些数据,如果再滚动,重复这个过程。
然后我们这个项目其实还用到过不等高的,不等高有一个问题,要确定滚动的container的高度,但是其实这个高度没必要一开始就要准确计算,只要告诉他能滚就好了。可以通过给一个每一行的估算高度,然后计算当前显示区域的item的高度,以及显示区域上面两个元素的高度,和下面的元素的高度。然后通过dom获取到他的高度后将他高度存起来,这个高度存起来有两个作用,第一个方面往回滚的高度的时候不用计算,第二个作用是向下滚动的时候,根据这个高度计算显示区域应该从哪个item开始显示,到哪个结束。
后来我们采用vue-virtual-scroller ,这个东西用起来一切都好,但是有一个问题就是大量滚动时闪白屏问题。
后来我又看了看这个源码,白屏的问题[juejin.cn/post/734350…]
核心思路其实就是上面的,然后其实iOS里面也有一个控件,其实就相当于前端的组件,iOS系统列表我们用的tableview 控件,api里面也有估算高度,核心逻辑也是这样,里面也是有循环利用,和v-for 里面dom更新差不多的原理,不过tableview滑动快不容易出现白边。
vue-virtual-scroller
白屏的问题
juejin.cn/post/734350…
扩展
-
为什么“需要渲染的节点越多,性能越差”?
-
浏览器回流重排、关键渲染路径等概念?
-
浏览器的渲染原理?
3.图片压缩脚本
项目背景是这样的,我们产品要求所有的图片都是无损的压缩,还有一个原因是安全, 代码和图片都放在一块,防止阿里云崩了, 图片访问不到,我们代码和图片放在一块,要崩一块崩。
我们之前打包都是手动压缩放本地,这样比较浪费时间,因为我人特别多,图片特别多,前端组内有五个,图片上千张,每次新加图片的时候都要手动压缩一下,很浪费时间,我就是想能不能做个脚本来自动的压缩。
对比了canvas ,webpack 本身的image-loader, 以及python-tinty库来做图片压缩,首先image-loader首先是ok的,来做这种自定义的图片压缩,但是他有一个问题是他是有损的,并且处理的时间比较长。
最后对比了canva库来做的,canvas 的压缩时长也要比tinty库慢20%的时间,所以我们用的python-tinty库来做的。其实这个tinty有node版本,也有python版本,我们为什么使用python版本而不用node版本呢,是因为python版本生态比较完善,比较稳定。node也可以用,社区用这个的比较少,所以就选用了python 版本。一开始我们使用手动构建的方式,手动构建的方式的主要问题是开始没办法和webpack结合。就每次要手动执行下python脚本,很麻烦。
最后我做了个优化,在webpack的钩子函数上,自定义构建步骤的时候执行python 的一个脚本,通过 child_process这一个包 exec(egze),这是一个命令,同步执行python的一个命令,阻塞我们整个打包的过程,等图片压缩完了之后再执行打包。这样可能会有一个问题,就是图片特别多,打包的时间会拉的特别长,所以又做了一个优化,就是做了缓存的优化,就是通过cache-loader的优化,通过cache-loader来做的,就是打包只压缩一次,后面发现图片有压缩过了,就直接跳过了。这样每次从打包图片2分钟,可能就变成5s钟了。
同时我又做了一个优化,图片去重,背景是一大推人往里面拖图片,就有可能拖的是同一张图片,那我们怎么解决图片重复的问题,也是用的python的脚本,就是他会去递归是检查是不是有图片是不是有重复的,一开始也是集成到webpack中去的,后来发现不可以这么做,就发现图片删除了,引用地址就出问题了,就会出现线上事故。执行的时候不是通过webpack,是通过手动的执行一个命令。 通过这个整体的优化,我们图片压缩方案推行了三个部门,4个项目。
不做用户上传的,而是做的UI出的图。
const path = require('path');
module.exports = {
entry: './src/index.js', // 入口文件
output: {
filename: 'bundle.js', // 输出的文件名
path: path.resolve(__dirname, 'dist') // 输出目录
},
module: {
rules: [
{
test: /\.(jpg|jpeg|png|gif|svg)$/i, // 匹配所有图像文件
use: [
{
loader: 'image-loader', // 使用 image-loader
options: {
// 配置选项,具体选项可参考文档
mozjpeg: {
progressive: true, // 开启渐进式 JPEG 图像
quality: 75 // 设置 JPEG 的质量(1-100)
},
optipng: {
enabled: true, // 启用 OptiPNG 压缩
},
pngquant: {
quality: [0.65, 0.9], // PNG 的压缩质量
speed: 4
},
gifsicle: {
interlaced: true, // 开启 GIF 格式的交错模式
},
}
}
]
}
]
}
};
疑问
- 生成缓存是根据文件名和文件内容,只要两个改一个,那我就要重新生成
- canvas 也有对应的node版本
- 去重是在打包前先去重,然后手动搜哪些地方用了这个删除的文件,手动修改
扩展问题
图片压缩算法
Tinify图片算法原理主要使用DEFLATE(difli)压缩算法进行的无损压缩。 DEFLATE 里面主要采用的是基于 LZ77 算法(字典替换)和 霍夫曼编码(Huffman Coding)进行的无损压缩。 LZ77 的基本思路是在一定范围内(滑动的查找范围),寻找是否已经出现过了,如果前面出现过,这里就不在直接完整记下这个数据,只需要记下来当前的数据是之前数据的从什么位置开始,长度记录下来,这样对于重复内容的比较多的压缩文件,压缩效果就比较好。
霍夫曼编码 用较短的编码表示频率较高的字符,用较长的编码表示频率较低的字符。 举个例子来说 比方说一个图片中有三种颜色,分别是用红,黄,蓝,那么我们可以统计颜色这三种出现的次数,然后出现次数多少依次按照编码成 0 10 110这样的二进制进行编码,与此同时还会有一个编码表,对应的 0 10 110 这些编码去存储什么颜色,然后去达到节省空间的目的。
这些数据不是按照字节去存储,而是按照位去存储的
有损压缩:上面说的是无损压缩,数据并没有丢失,只是换了一种方法编码,是存储这些信息的数据变小了。而有损压缩本质来说是丢弃部分数据来减少文件大小的算法。
有损压缩算法举例:
- 比方说对于JPEG 这种使用色彩空间使用的是YCbCr,Y 表示亮度(luma),Cb 和 Cr 分别表示色度(chrominance)。由于人眼对亮度敏感,而对色度不敏感,可以降低存储在每个像素中的色度信息,来达到图片压缩的目的。
如何编写 cache-loader ?
实现loader: 首先创建一个loader的js文件,然后定义一个方法,方法传入sourcecode,然后进行处理,处理完之后return处理结果,然后把该函数导出。 接着就可以去webpack配置了,配置主要在rules选项里面配置,用test选项配置要处理的文件,use配置该loader路径。
webpack打包流程
-
初始化 webpack会将从配置文件和 Shell语句中读取与合并参数,得出最终的参数
-
根据配置中的 entry 找出所有的⼊⼝⽂件,然后从这些⼊⼝⽂件出发,调⽤当前入口文件配置的 Loader 进行处理,如果这些模块还依赖的其他模块,递归的对这些依赖的模块进行处理,这一步处理完之后会得到了每个模块被翻译后的最终内容以及它们之间的依赖关系,
-
根据⼊⼝和模块之间的依赖关系,组装成⼀个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。
loader plugin 的区别
webpack默认只能加载js.json资源,其他资源就需要loader转换之后才能加载使用。 JS:
- ES6: babel-loader
- TS ts-loader
- 代码规范的 eslint-loader
- source-map-loader
CSS:
- sass,less - 预处理器 sassloader lessloader
- style-loader, css-loader
- post-css-loader
图片资源:
- file-loader
- url-loader
- image-loader
- file-loader:把⽂件输出到⼀个⽂件夹中,在代码中通过相对 URL 去引⽤输出的⽂件
- url-loader:和 file-loader 类似,但是能在⽂件很⼩的情况下以 base64 的⽅式把⽂件内容注⼊到代码中去
- source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试
- image-loader:加载并且压缩图⽚⽂件
- babel-loader:把 ES6 转换成 ES5
- css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性
- style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。
- eslint-loader:通过 ESLint 检查 JavaScript 代码
在Webpack中,loader的执行顺序是从右向左执行的。因为webpack选择了compose这样的函数式编程方式,这种方式的表达式执行是从右向左的。
vite rollup webpack
3.JSBridege 之间的交互
-
H5 和客户端的交互的方式其实也就是分两种情况,js调客户端,客户端调js。js这边原生的方法主要分为两种, 第一注入,native 往 webview 的 window 对象中添加一些原生方法,h5可以通过注入的方法来调用 app 的原生能力,第二个是拦截 H5通过与 native 之间的协议发送请求,native拦截请求再去调用 app 原生能力,比方说可以通过发送一些 LWZ:// XXX 请求或者通过 window.href 去做。 选型上交互我们这边移动端主要采用的WebViewJavascriptBridge 这个库,这个库最大好处是前端和安卓和iOS的交互方式是一样,要不你就每次交互都要分iOS 和安卓分别去实现。这个库在使用的时候基本就是使用注册方是使用registerHandler, 然后调用方 brige CallHandler.
-
我们项目中交互场景有这些,举几个例子。第一,app里面的H5界面的登录信息一般都是从客户端的拿的,一般是客户端在打开webview设置cookie,还有就是导航栏高度,也是客户端提供的。这里的cookie的过期时间是不写的,就是当前页面销毁就过期了。第二点:比方说可能H5跳转到原生界面,原生界面做一些,回到H5界面可能需要刷新数据,那就需要从H5带refresh参数过去,最后原生再传过来给H5. 还有这些当前直播带货模版,webview 弹框的高度是通过前端传递参数过去。这些里面不又少场景有相同的参数,还有一个我们app里面有很多场景,是H5调原生的情况,原生那边是通过也是通过路由的方式,我们把这些交互参数和路由参数整理了文档。方便后面新的需求,降低沟通成本。
-
性能优化方面:
除此之外,因为我懂客户端这块,我也做了iOS客户端的方面的优化 从打开webview的界面到显示内容经历了两个大的过程,第一个过程是原生webview组件的加载, 第二个过程是H5页面加载资源,渲染的时间。 所以工作的时候做过一段时间的iOS 性能监控的SDK监控数据,我们统计的webview的打开时间就会将webview的打开时间也加入,单纯依靠web 提高的 window.performance.timing的数据是不准确。 这里的第一个优化就是客户端那里,在app打开初始化一个全局的webview,后来去打开webview的时候就可以节省调前面的时间,对于iOS来说第二次打开webview 要比第一次打开快 700ms左右。
第二段时间就是前端常规的性能优化方式
降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
渲染:JS/CSS优化,加载顺序,服务端渲染模板直出。
如果前面的优化体验还不够,对于那些样式,功能不怎么变化的界面,那可以在客户端那里进行下载离线资源包,可以把css,部分js,存到客户端,包括里面的图片资源,也可以通过客户端比方说拦截这些网络请求,通过图片下载框架,把图片缓存到本地。 然后页面打开后只去下载会变化的数据,进一步优化,基本经过这个办法,就可以达到最好的效果。
- H5 和前端 交互 最好使用的方式:使用WebViewJavascriptBridge,因为iOS和安卓调用方式是一致的。
下面代码是js 调 oc
[_jsBridge registerHandler:@"colorClick" handler:^(id data, WVJBResponseCallback responseCallback) {
self.navigationController.navigationBar.barTintColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
responseCallback(@"颜色修改完毕!");
}];
WebViewJavascriptBridge.callHandler('colorClick',function(dataFromOC) {
alert("JS 调用了 OC 注册的 colorClick 方法");
document.getElementById("returnValue").value = dataFromOC;
})
iOS 两个控件 UIWebview WKWebview
oc->js
UIwebview
NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')", @"alert msg"];
[_webView stringByEvaluatingJavaScriptFromString:jsStr];
WKwebview
NSString *jsStr = [NSString stringWithFormat:@"setLocation('%@')", @"北京市东城区南锣鼓巷纳福胡同xx号"];
[_webview evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
NSLog(@"%@----%@", result, error);
}];
js -> oc 前端通过 windoow.location.herf ,客户端通过拦截网络请求的方式获取数据。 iOS wkwebview ,前端可以通过window.webkit.messageHandlers.xxx(name).postMessage(option);
// 原生第一步 要处理的地方
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"ScanAction"];
// 原生第二步 要处理的地方
// 处理方法
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
// message.body -- Allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull.
if ([message.name isEqualToString:@"ScanAction"]) {
NSLog(@"扫一扫");
} else if ([message.name isEqualToString:@"Location"]) {
[self getLocation];
} else if ([message.name isEqualToString:@"Share"]) {
[self shareWithParams:message.body];
} else if ([message.name isEqualToString:@"Color"]) {
[self changeBGColor:message.body];
} else if ([message.name isEqualToString:@"Pay"]) {
[self payWithParams:message.body];
} else if ([message.name isEqualToString:@"Shake"]) {
[self shakeAction];
} else if ([message.name isEqualToString:@"GoBack"]) {
[self goBack];
} else if ([message.name isEqualToString:@"PlaySound"]) {
[self playSound:message.body];
}
}
// 传null
function scanClick() {
// js 真正需要调用的地方
window.webkit.messageHandlers.ScanAction.postMessage(null);
}
// 传字典
function shareClick() {
window.webkit.messageHandlers.Share.postMessage({title:'测试分享的标题',content:'测试分享的内容',url:'http://www.baidu.com'});
}
// 传字符串
function playSound() {
window.webkit.messageHandlers.PlaySound.postMessage('shake_sound_male.wav');
}
// 传数组
function colorClick() {
window.webkit.messageHandlers.Color.postMessage([67,205,128,0.5]);
}
- webview的优化 首先webview的性能优化主要包括两个方面: 从打开webview的界面到显示内容经历了两个大的过程,第一段是原生webview组件的加载, 第二段是H5页面加载资源,渲染的时间。 所以工作的时候做过一段时间的iOS 性能监控的SDK监控数据,我们统计的webview的打开时间就会将webview的打开时间也加入,单纯依靠web 提高的 window.performance.timing的数据是不准确。 这里的第一个优化就是客户端那里,在app打开初始化一个全局的webview,后来去打开webview的时候就可以节省调前面的时间,对于iOS来说第二次打开webview 要比第一次打开快 700ms左右。
第二个就是前端的常规优化,
降低请求量:合并资源,减少 HTTP 请求数,minify / gzip 压缩,webP,lazyLoad。
加快请求速度:预解析DNS,减少域名数,并行加载,CDN 分发。
缓存:HTTP 协议缓存请求,离线缓存 manifest,离线数据缓存 localStorage。
渲染:JS/CSS优化,加载顺序,服务端渲染模板直出。
第三个就是前面的优化体验还不顾,那可以在客户端那里进行下载离线资源包,比方说当前界面变化不大,是不是可以把css,部分js,存到客户端,然后只去下载会变化的数据,进一步优化,基本经过这个办法,就可以达到最好的效果。
+ (void)onLaunchSyncTask:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions {
// 配置全局cookie
LWZWebConfiguration.shared.webCookie = [LWZWebCookie.alloc initWithCookies:@{
@"ver" : LWZConfigs.shared.webVer,
@"isOut" : @"0",
@"accountId" : [NSString stringWithFormat:@"%ld", (long)URAccount.shared.accountId],
@"UDID" : LWZConfigs.shared.UDID,
@"software" : LWZConfigs.shared.projectVer,
@"system" : LWZConfigs.shared.sysVer,
@"phoneModel" : LWZConfigs.shared.deviceModel,
@"platform" : LWZConfigs.shared.platform,
@"statusBarHeight" : [NSString stringWithFormat:@"%lf", UIApplication.sharedApplication.statusBarFrame.size.height],
@"tk" : URAccount.shared.loginToken ?:@"",
}];
// 配置公共参数
// 这几个参数是前期h5需要加的
// 后续都是通过cookie去处理的
[LWZWebConfiguration.shared setValue:@"0" forAdditionalParam:@"isOut"];
[LWZWebConfiguration.shared setValue:@"1" forAdditionalParam:@"v"];
[LWZWebConfiguration.shared setValue:[NSString stringWithFormat:@"%ld", (long)URAccount.shared.accountId] forAdditionalParam:@"accountId"];
webViewControllerHooks = LWZWebViewControllerHooks.alloc.init;
LWZWebConfiguration.shared.webViewControllerHooks = webViewControllerHooks;
}
4.大文件上传
背景:作为蓝舞者这个国标舞平台,我们经常需要将比赛的视频,按照半天或者一天的时间段去上传,由于这些视频的清晰度也比较高,总体体积会比较大,普遍在3.4个g,上传这些视频需要很长的时间,有时候上传了半个小时了,可能由于网络波动的原因,上传失败了,就要重新上传整个文件,对用户体验是非常糟糕的。为了提升用户的体验,我调研了业内文件上传的方案,对于文件上传业内的方案主要有,文件编码上传(比方说base64 编码或者其他方式),formData异步上传,切片上传。 对于文件编码的上传是非常的简单,前端去编码,后端去解码就可以完成,编码通常可能用base64比较多,用base64 有一个很大的问题会将文件大小增大三分之一,上传的时间更长了,这个我们不能接受。 formData ,他是通过键值对去传递数据,这个话很灵活,他的不足是他一旦建立连接,就不能终止,必须要一直上传完毕为止,他不能做断点续传, 这个也不满足我们的需求。 还有一个切片上传,他的好处是大文件切成多个小块,可以让每个小块分别上传,可以做断点续传,所以这个方案比较适合我们。 这里面有两个比较难的点,一个切片计算hash,一个是断点续传和文件秒传。 那我先说一下切片计算hash,
针对文件上传其实有三种方式,分别是文件编码上传(比方说base64 编码或者其他方式),formData异步上传,切片上传。
const input = document.querySelector('input[type="file"]');
input.addEventListener('change', function(event) {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const base64File = e.target.result;
// 通过Ajax发送Base64编码的文件
fetch('/upload', {
method: 'POST',
body: JSON.stringify({ file: base64File }),
headers: { 'Content-Type': 'application/json' }
}).then(response => response.json())
.then(data => console.log(data));
};
reader.readAsDataURL(file); // 转换文件为Base64
});
上面的方法核心就是将文件进行base64编码,然后组装成一个对象,然后序列化,然后利用Ajax上传。
formData 异步上传,他是通过键值对去传递数据,这个话很灵活,他的不足是他一旦建立连接,就不能终止,必须要一直上传完毕为止。这个对用户的来说肯定是不友好的,用户一旦有其他情况,想暂停或者过一会儿再传,这个就无法满足需求了。
// 获取表单元素
const fileInput = document.querySelector('input[name="file"]');
const usernameInput = document.querySelector('input[name="username"]');
const ageInput = document.querySelector('input[name="age"]');
// 创建 FormData 对象
const formData = new FormData();
// 将文件和其他字段添加到 FormData 对象
formData.append('file', fileInput.files[0]); // 添加文件
formData.append('username', usernameInput.value); // 添加文本字段
formData.append('age', ageInput.value); // 添加数字字段
// 使用 Fetch API 上传数据
fetch('/upload', {
method: 'POST',
body: formData // 直接将 FormData 对象作为请求体发送
}).then(response => response.json())
.then(data => console.log('上传成功:', data))
.catch(error => console.error('上传失败:', error));
创建一个FormData,然后利用
formData.append('file', fileInput.files[0]); // 添加文件这种直接把文件传进去,然后也可以以key 和value的方式添加进去别的字段,然后使用fetch api 上传.
还有一个切片上传,他的好处是大文件切成多个小块,然后通过网络将多个小块进行传输,传输的速度是很快的。这是第一个好处。第二个是大文件断点续传,支持用户中间进行暂停,回来继续上传,对用户体验也是非常好的,我们最终也是上传了切片上传。
技术选型:
大文件上传业内的方案有文件编码上传,他是利用FileReader API将文件转化为base64编码,将上传后的字符串发送给服务器,他的好处是无需刷新界面,可以实现异步上传。上传之前对文件进行编码处理,他的不足时对文件编码后数据变大了,导致传输的时间更长,而且服务端收到数据,需要对数据进行解码。 还有一种formData 异步上传, 它使用的js fomadata 通过ajax 将表单数据和文件异步上传到服务器,他的好处可以异步上传文件,可以方便增加其他字段,浏览器的兼容性比较好。他的不足不支持暂停,断点续传。对不支持formdata的浏览器可以使用其他方法进行处理。切片上传的好处是将大文件切成小块来进行上传,支持断点续传,即便在文件上传过程中断,也会重新联接,继续上传未上传的切片,无需上传整个文件,提高了上传的稳定性,通过对比,项目采用了切片上传的方案。
计算hash
先将文件使用slice 分成多个分片,项目是按照5M大小去分片,分片之后就针对这些这些分片计算hash,项目使用SparkMD5 进行hash计算,SparkMD5这个库是使用append buffer,然后按照分片数据按照顺序加入,计算hash。
由于js是单线程的,文件又很大,计算时长就很长,可能会造成卡顿问题。为了解决卡顿的问题,我们有两个解决途径,一个使用webwork,一个requestIdleCallback。webwork是将计算hash的操作放到新的线程中,可以根据cpu的核数多开几个线程进行处理。requestIdleCallback是在浏览器空闲时间才执行的。如果主线程长期繁忙(如动画、复杂渲染任务等),可能导致上传任务被延迟。我们的项目这里采用的是webwork。
上面两个方法只是解决了卡顿的问题,为了加快hash的计算过程,我们还可以采用SparkMD5里面的增量hash和抽样hash的方案。
为了提高hash的计算速度,可以采用增量hash和抽样hash。SparkMD5可以就是支持增量hash的,还有一个就是抽样hash。正常我都是读整个文件,而抽样的hash是读文件的首中末的部分,大概整个文件的80%,牺牲了一点的准确性,加快了速度。
普通的是把真个文件全部压进去计算hash。
断点续传和文件秒传
文件秒传,上传文件的时候,计算这个文件对应的hash,然后请求服务器哪些分片都上传过了,发现分片都全部上传成功,前端就可以去提示文件上传成功,接口中一般有一个字段标识当前的文件是否完整。
断点续传,只是上传了一部分,后端会将已经上传的分片的索引返回,然后前端看看哪些分片上传了,就不在上传,上传剩余没有上传的就可以了。
这个里也有一个优化,增加取消操作,可以取消当前的网络请求,对于axios,可以使用abortController 来取消请求,增强了可控性,再所有的切片都上传的情况下,调用接口通知后端。
大文件上传的一些思考
就是他并发的上传和http 2.0,可以使用promise.all 并发上传, http1.0 可以上传最多可以支持 6个,http 2.0就没有限制,支持多路复用。 文件碎片清理,有人上传一半,就暂停了,不要了,后端可以开启定时任务,比方说上传是一个月前的就可以删除掉。
webwork 的使用
onmessage = function(e) {
console.log('Worker received message:', e.data);
const result = e.data * 2; // 简单的计算
postMessage(result); // 将结果传回主线程
};
// 创建一个 Web Worker 实例
const worker = new Worker('worker.js');
// 向 Worker 发送消息
worker.postMessage(10); // 发送数据给 worker.js
// 监听 Worker 的返回结果
worker.onmessage = function(e) {
console.log('Result from worker:', e.data); // 输出:20
};
// 监听 Worker 错误
worker.onerror = function(e) {
console.error('Error in worker:', e.message);
};
5. 封装的组件库
组件的封装:
- 代码规范性 ESLint 和 Prettier的工具
- 扩展性,做东西肯定留插槽,作为后续的内容的补充
- 可维护性,做一个持续迭代的文档输出
组件的二次封装:
- 最重要的一点就是不能把原有的功能和方法搞得不能用了,vue可以通过
v-bind="$attrs"v-on="$listeners",slot 在封装的组件再写一遍就可以了
成功将工作效率提高了多少,在公司内部尽心推广。
6. excel 之间的交互
7.性能优化
前端的性能优化主要包括三个方面。
- 构建性能
- 运行性能
构建性能
对于构建性能,如果使用webpack来说,我们这里一般指的是构建速度的方面的优化。 提高打包速度方面:
-
首先可以设置缓存,提高二次构建性能。webpack 5之前是配置cache-loader,而现在简化了,只需要配置 cache 属性就可以了。
-
其次应该尽量采用并行处理方式,比如使用 Tread-loader,或者在 terser-webpack-plugin 中开启并行处理功能。
-
在减少编译范围方面可以,使用 noparse 跳过第三库编译
一些常规的优化方式有哪些?
1.图片方面,因为图片可能占了一个网站的很大一部分,所以图片这块很重要。
- 图片采用webp的方式
- 图片压缩,采用tiny库,进行压缩,当然这里也可以使用webpack 的imgloader进行压缩,同时也可以对css,js进行压缩 JS -> Terser, CSS -> cssnano,图片可以使用image-webpack-loader压缩图片大小
- 懒加载:只有当视图出现在视口中才去加载组件。图片懒加载也包含在内, 预加载:可以先把资源提前加载出来。(首页,详情页一些资源提前请求下来)
- treesharking - console.log 注释这些可以把这些去掉,没用的代码。treesharking依赖于ESModule。
- gzip压缩 他的压缩比是非常高,比方说你原有的是100mb,可能开启gzip压缩就50mb,所以这一步很关键,大大提高速度。他对于已经压缩过的图片这种效果不好,主要针对没有压缩过的资源。gzip是在服务器端开启的,如果是使用Nginx上开启的
- 缓存 在浏览器强缓存,协商缓存。也是nginx设置的。默认是get请求可以缓存。缓存不仅可以提高了访问了速度,也减少了对服务端的压力。control + f5 强制刷新
- CDN
- 分包
对于SPA 应用路由可以在根据 import()动态引用分包,比如对于SPA 应用 路由中可以使用 impor()方法,就可以将每一个视图所依赖的模块进行分包 还可以公共的代码分包提取出去 懒加载的方式导入模块,提高首屏的加载速度
- SSR 服务端渲染
- 采用http 2.0 10.字体可以使用压缩,和子集化(主要针对中文,比方说某个活动使用某种特殊字体,可以只把这几个字体弄出来就好)比方可以使用fonttools
性能指标
构建性能的指标重要有的:
- FP 第一个在浏览器绘制像素绘制时间点
- FCP 首次有用户能看到的内容绘制出现的时间
- LCP 最大的内容绘制的时间
- TTI 可交互时间页面加载完成且用户可以与页面进行交互的时间点,主线程空闲且页面响应用户输入。
- TBT 总阻塞时间页面加载过程中,主线程被长时间任务(通常是 JavaScript 执行)阻塞的总时间。
- CLS 累计布局偏移页面加载过程中发生的意外布局变化的总量,可能导致用户在交互时误触或出现不良体验。
- FID 首次输入延迟用户首次与页面交互(如点击按钮)时,页面响应用户输入所需的时间。
我们可以使用 lighthose,或者chrome提供的performance
首屏加载时间长怎么优化?
优化缓存策略,懒加载,预加载,服务端渲染,HTTP 缓存
- 懒加载 延迟加载不在视口中的资源,如图片、视频、JS 文件等
- 预加载 使用 提前加载关键资源
- CDN 使用内容分发网络提高静态资源加载速度
- http 缓存: ngxi (proxy cache) 可以开一个强缓存,协商缓存是后台也有相应的策略。
- SSR 服务端渲染 服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以 好处是减少客户端的 JavaScript 渲染时间,页面更快渲染,减少空白时间,还有一点是有利于SEO. 6.采用http 2.0