今天从多个方面来记录一下自己项目中考虑过的优化方案
一、代码层面
1、代码细节优化
首先从基本的代码层面来看,举个例子,比如我拿到了一个数组,需要对这个数组的内容做一些处理(比如需要对数组的内容遍历然后做一些处理等等),但是在当数组是空的时候就没有必要走下面的逻辑了。所以可以加一个判断语句,如果数组为空就不再进行后续的逻辑,这也可以减少很多代码的执行,从而提高性能。类似的还有很多,应该说是代码细节层面的一些优化了。
2、scoped时修改第三方组件样式
vue文件正常样式写在中,会被自动加上一个[data-v-xxxx]属性,但是第三方组件内部标签没有编译为这个属性,所以不能修改这个第三方组件。
为了父组件不影响子组件,用scoped,有一个方法是给第三方组件写class,然后在公共css或者当前页面写一个没有scoped的style,直接在里面修改第三方组件的样式,但是存在全局污染和命名冲突,约定的那个特殊的命名方式可以避免命名冲突。查找资料发现可以用深度选择器解决。深度选择器用/deep/,用到ElemntUI,又有预处理器就可以用.::v-deep。vue中过多使用scoped导致页面打包文件体积增大。通常能写在index中的样式尽量写在index中,我们可以通过在index样式中通过外层组件添加唯一class来区分组件+第三方样式来实现了类似于scoped的效果,又方便修改各种第三方组件的样式。
二、网络层面的优化
1、重复请求的取消:第一次请求还没回来之前就进行第二次同样的请求,这时候就应该把第二次请求给取消掉,防止重复请求
这里我们对axios请求进行了封装,采用了拦截器中进行了相应的处理。
在请求拦截器和响应拦截器之前,先实现了三个方法。
因为当请求方式,请求 URL和请求携带参数都一样时,就可以认为是相同的请求。因此在每次发起请求时,可以根据当前请求的请求方式、请求 URL地址和请求携带参数来生成一个唯一的 key;同时为每个请求创建一个专属的 CancelToken,然后将 key 和 Cancel 函数以键值对的形式保存到 Map对象中,使用 Map对象的好处是可以快速判断是否有重复的请求
// 用于根据当前请求的信息生成请求的key
generateReqKey(config){
const {method,url,params,data} = config;
return [method,url,qs.stringify(params),qs.stringify(data)].join('&')
}
// 用于把当前请求信息添加到pendingRequest对象中去
PendingRequest = new Map();
addPendingRequest(config){
const requestKey = this.generateReqKey(config);
config.cancalToken = config.cancalToken || new axios.CancelToken((cancel)=>{
if(!this.PendingRequest.has(requestKey)){
this.PendingRequest.set(requestKey,cancel)
}
})
}
// 检查是否存在重复请求,若存在则需要取消已经发出的请求
removeRequest(config){
const requestKey = this.generateReqKey(config);
if(this.PendingRequest.has(requestKey)){
const CancelToken = this.PendingRequest.get(requestKey);
CancelToken(requestKey);
this.PendingRequest.delete(requestKey);
}
}
然后是配置拦截器
因为需要对所有的请求都进行处理,因此考虑使用 axios的拦截器机制来实现取消重复请求的功能。
axios的制作者为开发人员提供了请求拦截器和响应拦截器,各自的作用如下:
(1)请求拦截器:该类拦截器的作用是在请求发送前统一执行某些操作,比如在请求头中添加 token字段;
(2)响应拦截器:该类拦截器的作用是在接收到服务器响应后统一执行某些操作,比如根据不同的状态码来执行相对应的操作。
interceptors(install) {
install.interceptors.request.use(config => {
// 检查是否存在重复请求
this.removeRequest(config);
// 把当前请求信息添加到pendingRequest对象中
this.addPendingRequest(config);
const token = localStorage.getItem('token');
if (token) {
config.headers.authorization = `token ${token}`;
}
return config
}, err => {
return Promise.reject(err)
});
install.interceptors.response.use(res => {
// 从pendingRequest对象中移除请求
this.removeRequest(res.config);
const { data } = res;
return data
}, err => {
// 从pendingRequest对象中移除请求
this.removeRequest(err.config)
return Promise.reject(err)
});
}
完整的网络请求封装代码如下:
import axios from 'axios';
// 引入qs库奖url中的参数转化为对象
import qs from "qs"
class HttpRequest {
constructor(options) {
this.defaults = {
baseUrl: ''
}
this.defaults = { ...this.defaults, ...options }
}
// 用于根据当前请求的信息生成请求的key
generateReqKey(config){
const {method,url,params,data} = config;
return [method,url,qs.stringify(params),qs.stringify(data)].join('&')
}
// 用于把当前请求信息添加到pendingRequest对象中去
PendingRequest = new Map();
addPendingRequest(config){
const requestKey = this.generateReqKey(config);
config.cancalToken = config.cancalToken || new axios.CancelToken((cancel)=>{
if(!this.PendingRequest.has(requestKey)){
this.PendingRequest.set(requestKey,cancel)
}
})
}
// 检查是否存在重复请求,若存在则需要取消已经发出的请求
removeRequest(config){
const requestKey = this.generateReqKey(config);
if(this.PendingRequest.has(requestKey)){
const CancelToken = this.PendingRequest.get(requestKey);
CancelToken(requestKey);
this.PendingRequest.delete(requestKey);
}
}
interceptors(install) {
install.interceptors.request.use(config => {
// 检查是否存在重复请求
this.removeRequest(config);
// 把当前请求信息添加到pendingRequest对象中
this.addPendingRequest(config);
const token = localStorage.getItem('token');
if (token) {
config.headers.authorization = `token ${token}`;
}
return config
}, err => {
return Promise.reject(err)
});
install.interceptors.response.use(res => {
// 从pendingRequest对象中移除请求
this.removeRequest(res.config);
const { data } = res;
return data
}, err => {
// 从pendingRequest对象中移除请求
this.removeRequest(err.config)
return Promise.reject(err)
});
}
request(options) {
options = { ...this.defaults, ...options }
const instance = axios.create(options);
this.interceptors(instance);
return instance
}
}
2、限制失败请求重试的次数,避免无效请求过多,导致大量的资源消耗
3、善用缓存,减小网络请求的次数比如在我们的项目中,个人空间展示页,如果判断是自己访问自己的空间,那么就不用再去请求数据了,直接从vuex中或者localstorage中去取就可以了。
4、断网处理
在vuex中存放network的状态,根据network状态判断是否需要加载断网组件,断网情况就加载断网组件,不加载对应页面组件,点击刷新,就跳转refresh页面然后立刻返回来实现重新获取数据,所以新建refresh.vue,在beforeRouteEnter钩子种返回当前页面。
<template>
<div id="app">
<div v-if="!network">
<h3>我没网了</h3>
<div @click="onRefresh">刷新</div>
</div>
<router-view/>
</div>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'App',
computed: {
...mapState(['network'])
},
methods: {
// 通过跳转一个空页面再返回的方式来实现刷新当前页面数据的目的
onRefresh () {
this.$router.replace('/refresh')
}
}
}
</script>
// refresh.vue
beforeRouteEnter (to, from, next) {
next(vm => {
vm.$router.replace(from.fullPath)
})
}
三、业务层面的优化
1、防抖节流
防抖节流是优化高频率执行代码的一种手段,比如浏览器的resize、scroll、keypress、mousemove等事件在触发的时候,会不断的调用绑定的回调函数,极大的浪费资源,甚至降低前端的性能。
为了优化体验,我们可以采取防抖和节流的手段来减少调用的频率。
节流:n秒内只运行一次,若n秒内重复触发,则只有一次生效。
防抖:n秒后执行该事件,若在n秒内重复触发,则重新计时。
代码实现如下:
节流函数
// 节流函数
function throttled(fn,delay) {
let timer = null;
let startTime = Date.now();
return function(){
let curTime = Date.now();
let remaining = delay-(curTime-startTime);
let context = this;
let args = arguments;
clearTimeout(timer);
if(remaining<=0){
fn.apply(context,args);
startTime = Date.now();
}else{
timer = setTimeout(fn,remaining);
}
}
}
防抖函数
// 防抖
function debounce(fn,wait,immediate){
let timeout;
return function(){
let context = this;
let args = arguments;
if(timeout) clearTimeout(timeout);
if(immediate){
let callNow = !timeout; //第一次回立即执行,以后只有事件执行之后才会继续触发
timeout = setTimeout(function(){
timeout = null;
},wait)
if(callNow){
fn.apply(context,args)
}
}else{
timeout = setTimeout(function(){
fn.apply(context,args);
},wait);
}
}
}
二者的应用场景举例:
防抖是连续的事件,只需要触发一次回调:
(1)搜索框搜索输入,只需要用户最后一次输入完再发送请求
(2)手机号、邮箱验证输入检测
(3)窗口大小resize,只需要窗口调整完成后,计算窗口大小,防止重复渲染
节流是每间隔一段时间执行一次回调:
(1)滚动加载,加载更多或者滚动到底部监听
(2)搜索框,搜索联想功能
2.事件解绑
组件销毁之前解绑事件,避免内存占用过多
3.长列表优化
我们的项目中采用了分页的思想来分批请求数据。其余的还可以采用懒加载、虚拟滚动等。
4.首屏时间优化
可以采用预渲染技术。
可以用lighthouse去测检测一下性能
5.路由懒加载
需要的时候进行加载,把不同路由对应的组件分成不同代码块,路由被访问才加载对应组件。路由会定义很多页面,页面打包后放到单独的js文件会导致非常大,懒加载把页面进行划分,需要时才加载,减少首页加载速度,懒加载主要就是把对应组件打包成js代码块进入首屏不用加载过度的资源,从而减少首屏加载速度。
// 引入路由组件
import Welcome from '@/pages/Welcome'
import Home from '@/pages/Home'; //引入组件会打包在文件中,如果都用import的话会导致所有组件打包在一个文件中,导致文件很大
// 按需加载,访问路径的时候才会加载;不访问不加载
const Search = ()=> import('@/pages/Search');
const Community = ()=> import('@/pages/Community');
const MyRoom = ()=> import('@/pages/MyRoom');
** 6.组件懒加载**
用到了再加载,缩短加载时间
四、打包方面
(1)减少打包的体积:压缩代码、资源的体积
(2)缩短打包时长:cache-loader、thread-loader
cache-loader: 允许缓存以下 loaders 到(默认)磁盘或数据库。在一些性能开销较大的 loader 之前添加 cache-loader,以便将结果缓存到磁盘里。保存和读取这些缓存文件会有一些时间开销,所以请只对性能开销较大的 loader 使用此 loader。
thread-loader:
使用时,需将此 loader 放置在其他 loader 之前。放置在此 loader 之后的 loader 会在一个独立的 worker 池中运行。
在 worker 池中运行的 loader 是受到限制的。例如:
这些 loader 不能生成新的文件。
这些 loader 不能使用自定义的 loader API(也就是说,不能通过插件来自定义)。
这些 loader 无法获取 webpack 的配置。
每个 worker 都是一个独立的 node.js 进程,其开销大约为 600ms 左右。同时会限制跨进程的数据交换。
请仅在耗时的操作中使用此 loader!
(3)使用CDN:请求资源更快