打包优化
移除 preload(预载) 与 prefetch (预取)
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压缩
安装:npm install compression-webpack-plugin --save-dev
// 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文件
<Connector
port="8080"
protocol="HTTP/1.1"
connectionTimeout="20000"
compression="on"
compressableMimeType="text/html,text/xml,text/javascript,text/css,text/plain,application/javascript"
useSendfile="false"/>
// compression="on" 打开压缩功能
// compressableMimeType="text/html,text/xml" 压缩类型
// useSendfile="false" 设置该属性将会压缩所有文件,不限阙值,不然可能按照阙值部分压缩
启用gzip压缩打包之后,会变成下面这样,自动生成gz包。目前大部分主流浏览器客户端都是支持gzip的,就算小部分非主流浏览器不支持也不用担心,不支持gzip格式文件的会默认访问源文件的,所以不要配置清除源文件。
配置好之后,打开浏览器访问线上,F12查看控制台,如果该文件资源的响应头里显示有Content-Encoding: gzip,表示浏览器支持并且启用了Gzip压缩的资源
公共代码抽离
代码抽离前 默认只生成两个文件,且vendors文件特别大,因为里面包含了我们所有的依赖包资源
代码抽离后 它会根据定义的模块规则进行代码抽离,可以看出抽离后的文件明显体积变小
执行抽离模块
chainWebpack: config => { config .when(process.env.NODE_ENV !== 'development', config => { config .plugin('ScriptExtHtmlWebpackPlugin') .after('html') .use('script-ext-html-webpack-plugin', [{ // `runtime` must same as runtimeChunk name. default is `runtime` inline: /runtime\..*\.js$/ }]) .end() config .optimization.splitChunks({ chunks: 'all', cacheGroups: { libs: { name: 'chunk-libs', test: /[\\/]node_modules[\\/]/, priority: 10, chunks: 'initial' // 当为initial时,只能分离初始化的模块,异步的模块无法分离 }, elementUI: { name: 'chunk-elementUI', // 将 elementUI 拆分为单个包 priority: 20, // 权重需要大于 libs 和 app 否则会被打包到 libs 或 app 中 test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // 匹配文件 }, echarts: { name: 'chunk-echarts', // 将 echarts 拆分为单个包 priority: 20, test: /[\\/]node_modules[\\/]_?echarts(.*)/ }, commons: { name: 'chunk-commons', test: resolve('src/components'), // 可以自定你的规则 minChunks: 3, // 当某个模块满足minChunks引用次数时,才会被打包。 priority: 5, reuseExistingChunk: true // 默认为false,关闭表示拆分出复用部分的模块,给双方引用 } } }); // 最小化代码 config.optimization.minimize(true); // 如果我们要在一个 HTML 页面上使用多个入口时,还需设置 optimization.runtimeChunk: 'single',否则还会遇到这里所述的麻烦。 // https://bundlers.tooling.report/code-splitting/multi-entry/ // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk config.optimization.runtimeChunk('single') } );}
配置解析chunks:可用值为“all”、“async”、“initial”,默认值是“async”
all:拆分同步和异步文件 async:只对异步文件作处理 initial:只对入口文件进行拆分,不去处理异步文件里的模块
** minSize**:控制最小包的大小,大于这个值才会去拆分,
如果拆分的公共模块小于这个大小,那就复制成多份,
直接打包到引用该模块的包里
** minChunks**:模块的重复调用次数大于等于minChunks值时,就会满足这项拆包条件,
但只看入口模块导入的,不看动态加载模块中导入的(import(‘…’)),
即使设置的chunks为“all”。
** maxInitialRequests**:入口文件最大请求的文件数量(import等方式)
入口文件本身算一个请求
入口文件动态加载的模块不算在内
通过runtimeChunk拆分出来的runtime文件不算在内
只算js,css不算在内
如果同时有两个模块满足cacheGroups的拆分规则,
但maxInitialRequests只允许再拆分一个,那么会拆出体积
更大的那个模块。
** maxAsyncRequests**:用于限制异步模块内部的并行最大请求数
import文件本身算一个请求
只算js、css不算在内
如果同时有两个模块满足cacheGroups的规则需要拆分,
但maxAsyncRequests只允许拆分一个时,那么会拆出体积更大的那个模块。
** name**:主要用于分离chunks后的名字,可以是字符串或者函数,
相同name会合并成一个chunk
** splitChunks.cacheGroups**
可以继承或者重写splitChunks对象下的属性,但是test、priority和reuseExistingChunk
只能配置在cacheGroup对象中
每个添加的对象都有默认配置,如果想禁用此配置,可以将其设置为false
CDN替换依赖包引入
项目打包时会根据依赖关系自动打包压缩依赖文件,当依赖文件过大,会导致首屏加载变慢
使用CDN: 简单来说就是可以使用CDN引入依赖库,减少依赖包体积,释放服务器压力, 它由距离最近的缓存服务器直接响应,提高加载速度
进一步考虑:当我们在开发环境下,使用CDN引入会比我们直接引入依赖要慢,所以配置CDN需要只在生产环境
配置步骤:
// 环境区分主要为开发环境与其他环境(其他:生产,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
})
}
修改 public/index.html(根据环境,开发环境不使用CDN,生产环境才开放)
<html>
<head>
<!-- 样式文件优先加载 -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
<% } %>
</head>
<body>
<div id="app"></div>
<!-- js加载 -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
</body>
</html>
采用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'
Uncaught TypeError: Illegal constructor
解决方案:修改externals 中‘'element-ui’的value为:ELEMENT
移除无用文件
安装:npm install useless-files-webpack-plugin --save-dev
// 导入插件
const UselessFile = require('useless-files-webpack-plugin')
chainWebpack: config => {
config.plugin('uselessFile')
.use(
new UselessFile({
root: './src', // 项目目录
out: './fileList.json', // 输出文件列表
clean: false, // 是否删除文件,
exclude: [/node_modules/] // 排除文件列表
})
)
}
路由优化
动态路由懒加载,按需加载
单页面应用的首屏加载较慢主要是因为单页面应用在首屏时,无论是否需要都会加载所有的模块,可通过按需加载、路由懒加载来优化。
- 动态加载,通过import来实现路由的动态加载,这个时候对于路由的加载是动态的,用到再加载。
{、 path:"/home", name:"home", component: () => import(/* webpackChunkName: "home" */ "@/views/home.vue"))}
- 按需加载,通过对路由文件的配置,来对相关模块划分区间,如登录界面可以和首页、主页面划分一块,在进入首屏时,只对首屏所在的区块进行加载。通过require.ensure()来将多个相同类的组件打包成一个文件。如示例代码,打包时,将两个组件打包成一个js文件,文件名为home
{ path: '/login', //path路径 name: 'login', //组件名 component: r => require.ensure([], () => r(require('../components/login')), 'home') //good类型的组件},{ path: '/home', //path路径 name: 'home', //组件名 component: r => require.ensure([], () => r(require('../components/home')), 'home') //good类型的组件}
动态添加路由
通常路由权限实现可以通过路由守卫 router.beforeEach,配合使用 **router.addRoutes **动态添加路由
import router from './router'
import store from './store'
router.beforeEach(async(to, from, next) => {
const hasToken = getToken()
if (hasToken) {
if (to.path === '/login') {
next({ path: '/' })
} else {
// 判断用户是否通过 getInfo 获取了自己的角色权限
const hasRoles = store.getters.roles && store.getters.roles.length > 0
if (hasRoles) {
next()
} else {
try {
// 获取用户角色
const { roles } = await store.dispatch('user/getInfo')
// 获取用户具有权限的路由
const accessRoutes = await store.dispatch('permission/getUserMenus', roles)
// 动态添加可访问的路由
router.addRoutes(accessRoutes)
// 确保addRoutes完整的hack方法
// 设置 replace: true,因此导航不会留下历史记录
next({ ...to, replace: true })
} catch (error) {
// 移除token,进入登录页面重新登录
await store.dispatch('user/resetToken')
console.error(error)
next(`/login?redirect=${to.path}`)
}
}
}
}
})
版权归原作者 菜下饭 所有, 如有侵权,请联系我们删除。