代码分割可以将代码按照不同的逻辑或业务分割成不同的模块,从而实现按需加载,减少页面加载时间。在Vue中,可以使用以下方式进行代码分割:
使用Vue异步组件:将组件定义为函数返回一个Promise对象,在需要的时候再加载组件。
Vue.component('async-component', () => import('./AsyncComponent.vue'));
使用Webpack的代码分割功能:在Webpack配置文件中配置optimization.splitChunks进行代码分割。
optimization: {
splitChunks: {
chunks: 'all'
}
}
代码分割实例:
1 在项目中,有一个比较大的组件需要加载,但是这个组件并不是每个页面都需要用到。在使用Vue异步组件进行代码分割之前,每次进入页面都需要加载这个组件,导致页面加载时间过长。使用Vue异步组件进行代码分割后,只有在需要用到这个组件的时候才会进行加载,从而减少了页面加载时间。
2 在项目中,有多个页面都需要使用到一个公共的模块,但是这个模块比较大,每个页面都将这个模块打包进去会导致打包后的文件过大。使用Webpack的代码分割功能进行代码分割后,公共的模块只会被打包一次,从而减小了打包后文件的大小。
懒加载是将某些组件或资源推迟到实际需要的时候再加载,可以有效减少首屏加载时的资源压力。
可以使用 Vue.lazy()
方法实现懒加载,也可以使用第三方库如 vue-lazyload
。
- Tree Shaking是什么?
假设有一个工具库utils.js,其中包含了add、sub、mul、div四个函数,我们只需要使用add和sub函数,可以这样引入:
import { add, sub } from 'utils.js'
在打包过程中,只有add和sub函数会被保留,mul和div函数会被去除。
vue-lazyload
插件实现图片懒加载src
属性改为v-lazy
指令loading
属性来展示图片加载过程中的占位图error
属性来展示图片加载失败时的占位图
// 在数据不需要响应式化时使用Object.freeze()
export default {
data() {
return {
// 不需要响应式化的数据
list: Object.freeze(['apple', 'banana', 'orange'])
}
}
}
商品数量:{{ count }}
商品总价:{{ totalPrice }}
计数器:{{ count }}
显示内容
显示内容
// 环境区分主要为开发环境与其他环境(其他:生产,uat,测试等等)
const isNotDevelopMentEnv = process.env.NODE_ENV !== 'development'
const cdnData = {
css: [
'https://cdn.bootcdn.net/ajax/libs/element-ui/2.13.0/theme-chalk/index.css'
],
js: [
'https://cdn.bootcdn.net/ajax/libs/vue/2.6.10/vue.min.js',
'https://cdn.bootcdn.net/ajax/libs/axios/0.19.2/axios.min.js',
'https://cdn.bootcdn.net/ajax/libs/vuex/3.1.0/vuex.min.js',
'https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.6/vue-router.min.js',
'https://cdn.bootcdn.net/ajax/libs/element-ui/2.13.0/index.js',
'https://cdn.bootcdn.net/ajax/libs/jquery/1.12.1/jquery.min.js',
'https://cdn.bootcdn.net/ajax/libs/vee-validate/2.0.0-rc.21/vee-validate.min.js',
'https://cdn.bootcdn.net/ajax/libs/vee-validate/2.0.0-rc.21/locale/zh_CN.js'
],
/**
* 属性名称 vue, 表示遇到 import xxx from 'vue'
* 这类引入 'vue'的,不去 node_modules 中找,而是去找全局变量 Vue
* 其他的为VueRouter、Vuex、axios、ELEMENT、echarts,注意全局变量是一个确定的值,不能修改为其他值,修改为其他大小写或者其他值会报错
*/
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'element-ui': 'ELEMENT',
'vuex': 'Vuex',
'axios': 'axios',
'vee-validate': 'VeeValidate',
'jQuery':"jquery",
'jquery': 'window.$'
}
}
// 在configureWebpack中添加externals
configureWebpack: {
externals: isNotDevelopMentEnv ? cdnData.externals : {}
}
// 在chainWepack中添加如下
if (isNotDevelopMentEnv) {
config.plugin('html')
.tap(args => {
args[0].cdn = cdnData
return args
})
}
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<% } %>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<% } %>
采用CDN引入后,不需要删除原有依赖引入,因为在本地还是使用这些依赖进行调试的,打包后因为有CDN所以不会把这些依赖引入所以不用担心,import引入的不需要变更。例如main.js中使用import ElementUI from 'element-ui', 以上的代码已经实现在开发环境会设置不适用CDN,会使用依赖包文件;当发布到生产环境,因为我们已经在vue.config.js的externals中指代了element-ui,所以这个语句也是有效的可以直接使用CDN elementUI
易出错点:
Router is not defined
解决方案: 将Router 改为 'VueRouter'
解决方案:修改externals 中‘'element-ui’的value为:ELEMENT
gzip on;
gzip_types text/plain application/xml text/css application/javascript;
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/xml
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/x-javascript
- 3.2 代码优化的实践
vue 脚手架默认开启了 preload 与 prefetch,对于小项目可以提升体验感,但当我们项目很大时,首屏加载就会很慢很慢。
先简单了解一下 preload 与 prefetch。
preload 与 prefetch 都是一种资源预加载机制;
preload 是预先加载资源,但并不执行,只有需要时才执行它;
prefetch 是意图预获取一些资源,以备下一个导航/页面使用;
preload 的优先级高于 prefetch。
配置文件:vue.config.js
chainWebpack: config => {
// 移除 preload(预载) 插件
config.plugins.delete('preload')
// 移除 prefetch(预取) 插件
config.plugins.delete('prefetch')
}
npm install webpack-bundle-analyzer --save-devg
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
chainWebpack: config => {
// 添加资源可视化工具
config.plugins.push(
new BundleAnalyzerPlugin()
)
}
它可以查看资源模块的体积分布,然后可以对应做优化处理。
// gzip压缩插件
const CompressionWebpackPlugin = require('compression-webpack-plugin')
chainWebpack: config => {
// 添加gzip压缩插件
config.plugins.push(
new CompressionWebpackPlugin(
{
filename: info => {
return `${info.path}.gz${info.query}`
},
algorithm: 'gzip',
threshold: 5120, // 只有大小大于该值的资源会被处理 10240
// test: new RegExp('\\.(' + ['js'].join('|') + ')$'),
test: /\.js$|\.html$|\.json$|\.css/,
minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理
deleteOriginalAssets: false // 删除原文件
}
)
)
}
开启gzip压缩,需要配置nginx,打开nginx.config文件,写入以下(在http块内或者在单个server块里添加)
#开启gzip
gzip on;#低于1kb的资源不压缩
gzip_min_length 1k;#压缩级别1-9,越大压缩率越高,同时消耗cpu资源也越多
gzip_comp_level 9;#需要压缩哪些响应类型的资源,多个空格隔开。不建议压缩图片.
gzip_types text/plain application/javascript application/x-javascript text/javascript text/xml text/css;#配置禁用gzip条件,支持正则。此处表示ie6及以下不启用gzip(因为ie低版本不支持)
gzip_disable "MSIE [1-6]\.";
#是否添加“Vary: Accept-Encoding”响应头
gzip_vary on;
开启gzip压缩,服务器为tomcat,修改server.xml文件
// compression="on" 打开压缩功能
// compressableMimeType="text/html,text/xml" 压缩类型
// useSendfile="false" 设置该属性将会压缩所有文件,不限阙值,不然可能按照阙值部分压缩
启用gzip压缩打包之后,会变成下面这样,自动生成gz包。目前大部分主流浏览器客户端都是支持gzip的,就算小部分非主流浏览器不支持也不用担心,不支持gzip格式文件的会默认访问源文件的,所以不要配置清除源文件。
配置好之后,打开浏览器访问线上,F12查看控制台,如果该文件资源的响应头里显示有Content-Encoding: gzip,表示浏览器支持并且启用了Gzip压缩的资源
data 中的数据初始化会增加getter 和 setter,会收集对应的watcher,值改变时整个应用会重新渲染,可以使用computed (当新的值需要大量计算才能得到,缓存的意义就非常大)
{{ result }}
v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
key存在意义:为了跟踪每个节点的特征,使其具有唯一性,高效更新虚拟dom
vue在更新已经渲染的元素序列时,会采用就地复用策略,都会在对顺序进行破坏时,不仅会产生真实dom更新,浪费资源,从而导致产生错误更新。
比如两个inputAB输入值,在头部添加一个InputC,结果按顺序,CA有值,B无值。
//哪怕我们只渲染一小部分元素,也得在每次重新渲染的时候遍历整个列表。如果 list 的数据有很多,就会造成性能低,页面可能卡顿的现象出现。
//每一次都这样运算
this.list.map( item=> {
if (item.active) {
return item.name
}
});
//解决办法
//1.使用空标签 template.
{{item.name}}
//2.使用compted过滤属性
computed:{
items:function(){
return this.list.filter(item=>{
return item.show
})
}
}
- 如果列表是纯粹的数据展示,不会有任何改变,就不需要做响应化
export default {
data: () => ({
users: []
}),
async created() {
const users = await axios.get("/api/users");
// 浅冻结
this.users = Object.freeze(users);
}
};
// 深冻结函数
function deepFreeze(obj) {
var propNames = Object.getOwnPropertyNames(obj);
propNames.forEach(function (name) {
var prop = obj[name];
// 如果prop是个对象,冻结它
if (typeof prop == 'object' && prop !== null)
deepFreeze(prop);
});
return Object.freeze(obj);
}
可以处理不分页的10w条数据, 也可以处理分页的数据
> 参考[vue-virtual-scroller](https://github.com/Akryum/vue-virtual-scroller)、[vue-virtual-scroll-list](https://github.com/tangbc/vue-virtual-scroll-list)
Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。
created() {
this.timer = setInterval(this.refresh, 2000)
},
beforeDestroy() {
clearInterval(this.timer)
}
只有在使用该路由时才加载路由。可缩减首屏加载时间。
const router = new VueRouter({
routes: [
{ path: '/foo', component: () => import('./Foo.vue') }
]
})
keep-alive是vue
中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染dom;
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。
> 参考项目:[vue-lazyload](https://github.com/hilongjw/vue-lazyload)
我们大家都知道我们vue页面中所有的dom都是通过js执行然后才渲染完成的,我们的html代码实际上仅仅只有几十行,那么我们如果加载js时很慢页面就会出现一段白屏阶段,那么这个白屏阶段给用户的体验就不是很好,我们是否可以给一些显示给用户看呢?
我们可以在项目中加入一个加载动效的动画图片在静态文件中,然后在html文件的app这个div中加入这个图片让它动就完事了,我们可以在app这个div中随便写东西,因为我们知道当一个元素被作为模板时,它原先里面的内容就会全部被覆盖,所以我们随便写:
当我们的js加载完成之后我们的img就会被覆盖了,所以这种效果是特别好的,因为静态资源是在我们的项目当中直接有的,所以加载效率会特别的快,所以尽量让图片的大小变小这样会更好的提高项目效率
按需加载: 利用异步组件和路由懒加载,将不同路由或组件的加载推迟到实际需要时。这将显著降低初始加载体积,让用户能够更快地看到页面内容。
const routes = [
{
path: '/home',
component: () => import('./Home.vue')
},
// 其他路由...
];
提前加载: Vue 3的路由支持预加载功能,可在用户浏览站点时预先加载下一个页面所需的资源。这将确保用户切换页面时的迅速加载和呈现。
const routes = [
{
path: '/home',
component: () => import('./Home.vue'),
meta: { preload: true }
},
// 其他路由...
];
模块拆分: 配置Webpack将代码拆分成多个小块,利用Tree Shaking、代码压缩等技术减少代码体积。这将减少初始加载所需的下载时间,提高页面加载速度。
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
};
类型 | 场景 |
---|---|
函数防抖 | 搜索框输入(只需要最后一次输入完成后再放松Ajax请求) |
- | 滚动事件scroll(只需要执行触发后的最后一次滚动事件的处理程序) |
- | 连续点击按钮事件 |
- | 窗口resiz改变事件 |
- | 文本输入的验证(连续输入文字后发送Ajax请求进行验证,停止输入后验证一次) |
函数节流 | DOM元素的拖拽功能实现(mousemove) |
- | 游戏中的刷新率,比如射击游戏,就算一直按着鼠标射击,也只会在规定射速内射出子弹 |
- | Canvas画笔功能 |
- | 鼠标不断触发某事件时,如点击,只在单位事件内触发一次. |
防抖
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时
防抖类似于王者英雄回城6秒,如果回城被打断,再次回城需要再等6秒
应用场景
//定义方法要做的事情
function fun(){
console.log('我改变啦')
}
//定义事件触发要执行的方法,两个参数分别是传入的要做的事情和定时器的毫秒数
function debounce(fn,delay){
//定义一个变量作为等会清除对象
var flay=null;
//使用闭包防止每次调用时将flay初始化为null;
return function(){
//在这里一定要清除前面的定时器,然后创建一个新的定时器
clearTimeout(flay)
//最后这个定时器只会执行一次
//事件触发完成之后延迟500毫秒再触发
//(这里的变量赋值是跟定时器建立连接,进行地址赋值,要重新赋值给flay
//重新设置新的延时器
flay=setTimeout(function(){
fn.apply(this); //修正fn函数的this指向
},delay)
}
}
//给浏览器添加监听事件resize
window.addEventListener('resize', debounce(fun, 500));
节流
节流 规定在给定时间内,只能触发一次函数。如果在给定时间内触发多次函数,只有一次生效
节流类似于王者技能,放完一个英雄技能后,要6秒才能再次释放
//时间戳
//fn为需要执行的函数,wait为需要延迟的时间
function throttle(fn,wait){
//记录上一次函数触发的时间
var pre = Date.now();
//使用闭包能防止让pre 的值在每次调用时都初始化
return function(){
var context = this;
var args = arguments;
//arguments 对象包含了传给函数的所有实参
//arguments 是函数调用时传入的参数组成的类数组对象
//func(1, 2),arguments 就约等于 [1, 2]
var now = Date.now();
if( now - pre >= wait){
//修正this的指向问题
fn.apply(context,args);
//将时间同步
pre = Date.now();
}
}
}
//定义要做的事情
function handle(){
console.log(Math.random());
}
//触发函数
window.addEventListener("mousemove",throttle(handle,1000));
//定时器
function throttle(fn,wait){
var timer = null;
return function(){
var context = this;
var args = arguments;
if(!timer){
timer = setTimeout(function(){
fn.apply(context,args);
timer = null;
},wait)
}
}
}
function handle(){
console.log(Math.random());
}
window.addEventListener(“mousemove”,throttle(handle,1000));
像element-ui这样的第三方组件库可以按需引入避免体积太大。
import Vue from 'vue';
import { Button, Select } from 'element-ui';
Vue.use(Button)
Vue.use(Select)
npm i uglifyjs-webpack-plugin
在webpack.config.js
文件下进行如下配置。
// 注:vue版本:“vue”:“^2.5.2”,webpack版本:“webpack”:" ^3.6.0"
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
// 省略...
mode: "production",
optimization: {
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
// 删除注释
output:{
comments: false
},
compress: {
warnings: false,
drop_console: true, // 删除所有调式带有console的
drop_debugger: true, //去除打包后的debugger
pure_funcs: ['console.log'] // 删除console.log
}
}
})
]
}
}
预渲染
vue是一个单页面应用(spa),只有一个 html 文件(内容只有一个#app根节点),通过加载js脚本来填充页面要渲染的内容,然而这种方式无法被爬虫和百度搜索到。
构建阶段生成匹配预渲染路径的 html 文件(注意:每个需要预渲染的路由都有一个对应的 html)。构建出来的 html
文件已经有静态数据,需要ajax数据的部分未构建
解决问题
//1.安装预渲染插件
npm install prerender-spa-plugin -D #安装或者编译出错,npm换成cnpm
//一个 webpack 插件用于在单页应用中预渲染静态 html 内容。因此,该插件限定了你的单页应用必须使用 webpack 构建,且它是框架无关的,无论你是使用 React 或 Vue 甚至不使用框架,都能用来进行预渲染。
//原理:在 webpack 构建阶段的最后,在本地启动一个 phantomjs,访问配置了预渲染的路由,再将 phantomjs 中渲染的页面输出到 html 文件中,并建立路由对应的目录。
//2.配置vue.config.js
const path = require('path')
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = PrerenderSPAPlugin.PuppeteerRenderer
module.exports = {
configureWebpack: {
plugins: [
new PrerenderSPAPlugin({
// 生成文件的路径,与webpack打包一致即可
// 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
staticDir: path.join(__dirname, 'dist'),
// 需要预渲染的路由
// 对应自己的路由文件
routes: ['/', '/about'],
// 这个很重要,如果没有配置这段,也不会进行预编译
renderer: new Renderer({
inject: {
foo: 'bar'
},
//renderer.headless为表示是否以无头模式运行,无头即不展示界面,如果设置为false,则会在浏览器加载页面时候展示出来,一般用于调试
headless: false,
//renderer.renderAfterTime可以让控制页面加载好后等一段时间再截图,保证数据已经都拿到,页面渲染完毕
renderAfterTime: 5000,
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),
//两者的事件名称要对应上。在程序入口执行
renderAfterDocumentEvent: 'render-event',
})
})
]
}
}
//4.修改main.js
new Vue({
el: '#app',
router,
components: { App },
template: ' ',
// 添加mounted,不然不会执行预编译
mounted () {
document.dispatchEvent(new Event('render-event'))
}
})
//5.相关路由文件
export default new Router({
mode: 'history',
routes
})
//npm run build
看一下生成的 dist 的目录里是不是有每个路由名称对应的文件,有就对了
小知识:seo为啥对vue单页面不友好?
服务端渲染 SSR,nuxt.js
服务端渲染:网页上面呈现的内容在服务器端就已经生成好了,当用户浏览网页时,服务器把这个在服务端生成好的完整的html结构内容响应给浏览器,而浏览器拿到这个完整的html结构内容后直接显示(渲染)在页面上的过程
SSR=> 后端把.vue文件编译成.html文件返回给前端渲染,它的好处就是有利于SEO