跟着Vue-cli来'学'并'改'Webpack之 打包优化
首先,我们要知道什么要用webpack来打包,这样打包有那些好处。我们可以简单的列出以下几点:
- 单文件组件 (.vue文件)
- 优化Vue构建过程 (alias等)
- 浏览器缓存管理
- 代码分离 (懒加载等)
这篇文章的重点讲的就是webpack打包之优化浏览器缓存管理,vue-cli生成的脚手架的配置中,已经做了很多对于打包,利用缓存的优化处理,本文将来学校其中知识,并且做出改动。
了解浏览器的缓存原理
在此之前呢,我们需要先了解浏览器缓存是怎么工作的,先抄了一张图。
- 浏览器: 我需要 test.js
- 服务器:找到了给你,并且在259200秒(一个月)内别来找我
- 浏览器:好的,那我缓存到磁盘里
过了一个星期,再次访问这个页面
- 浏览器:我需要test.js,缓存期限还在,直接从磁盘读取
- 服务器:没我卵事
- 用户:哇塞打开页面好快
应产品经理需求更改了一个图标
- 浏览器:我需要test.js,缓存期限还在,直接从磁盘读取
- 产品经理:发布了吗?你确定?怎么没效果啊?
- 服务器:吃瓜
弄清了原理,我们就知道怎么去破坏缓存机制,让浏览器请求到新的文件。
清楚缓存技术
ctrl+F5 强制刷新页面
手动强制刷新页面,但是用户不是程序员啊,他们怎么会知道需要强制刷新呢,所以这个方案给用户肯定是不可行的。
更改文件
- 修改文件的名字:test.js -> test.v2.js
- 修改文件的路径:/static/test.js -> /static/v2/test.js
- 加 query string : test.js -> test.js?v=qwer
我们了解完了如何清楚缓存,再来看看Vue-cli模版中是如何进行配置的
Code Splitting(代码分割)
什么是代码分割
我们直接生成一个Vue-cli的新项目,安装依赖后直接运行 npm run build命令,并打开/dist/js文件目录
发现有3个js文件,这就是webpack将代码进行了分割。
为什么要进行代码分割
- 分离业务代码和第三方库( vendor )
- 按需加载(利用 import() 语法)
之所以把业务代码和第三方库代码分离出来,是因为产品经理的需求是源源不断的,因此业务代码更新频率大,相反第三方库代码更新迭代相对较慢且可以锁版本,所以可以充分利用浏览器的缓存来加载这些第三方库。
而按需加载的适用场景,比如说「访问某个路由的时候再去加载对应的组件」,用户不一定会访问所有的路由,所以没必要把所有路由对应的组件都先在开始的加载完;更典型的例子是「某些用户他们的权限只能访问某些页面」,所以没必要把他们没权限访问的页面的代码也加载
剖析 vue-cli 的 webpack Code Splitting
分离业务代码和第三方库( vendor )
vue-cli中使用了 CommonsChunkPlugin这个webpack插件来提取框架代码。 打开 webpack.prod.conf.js 文件,找到下面这段代码
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
这段代码在打包的时候把 node_modules 下面的 ,并且名字是 .js 结尾的,并且不是重复的模块提取到vender
里面。
所以打包后应该会生成app.js(业务代码)、vender.js(框架代码)这个两个文件,细心的同学可能会发现还有个 manifest.js,在后面我们讲进行解读。
按需加载(利用 import() 语法)
如果我们修改一下hello组件的加载方式改为路由懒加载(import()语法),在进行打包
// import Hello from '@/components/Hello'
export default new Router({
routes: [
{
path: '/',
name: 'Hello',
// component: Hello,
component: () => import('@/components/Hello')
}
]
})
很明显的看到,打包后有4个js文件,仔细的同学还发现,app.js文件的大小加上新多出文件的大小,正好等于没有分割打包的app的大小。 这样等于异步加载的组件,是单独打包成了一个js,在页面首次加载的时候不需要加载他,等到请求相应的页面的时候在去服务器请求它,减小了页面首屏加载的时间。
vue-cli /webpack.prod.conf.js
中配置 output.chunkFilename 规定了打包异步文件的格式
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
如何利用利用浏览器缓存
目标
假设我们现在有很多很多的静态文件,然后每次需要更新很多很多的文件,那是不是要手动地一个一个地修改文件的名字呢?我们的理想当然是:哪个文件更新了,就自动地生成一个新的文件名。
另外,如果我们打包出来的静态文件只有一个单独的 JavaScript 文件 app.js ,那么每次改动一点代码,app.js 的文件名肯定都会变。但实际上,我只改动了某个模块的代码(其他模块并没有修改),就破坏了其他模块的缓存,这显然没有充分利用到缓存啊。我们的目标是:
哪个模块更新了破坏他的缓存,没更新的模块继续利用缓存。
步骤1:增加hash值
上文中我们提到清楚缓存的三种方式:修改文件名,修改路径,给url加参数,webpack的做法是修改文件名。
vue-cli /webpack.prod.conf.js
中 output.chunkFilename 规定了打包异步文件的格式
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
// 规定文件名为 js文件夹下 Chunk.name . hash值 .js 的文件
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
// 规定文件名为 js文件夹下 module id. hash值 .js 的文件
},
这样的话给每个文件加上了hash值,那个文件发生了变化hash值就会改变
步骤2:提取manifast文件
为什么要提取manifast文件呢?
原因是 vendor chunk 里面包含了 webpack 的 runtime 代码(用来解析和加载模块之类的运行时代码)
这样会导致:即使你没有更改引入模块(vendor的模块没有发生变动的情况下,你仅仅修改了其他代码) 也会导致 vendor
的chunkhash值发生变化,从而破坏了缓存,达不到预期效果
vue-cli /webpack.prod.conf.js
提取 manifast
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
步骤3:根据模块的相对路径生成一个四位数的hash作为模块id
webpack 里每个模块都有一个 module id ,module id 是该模块在模块依赖关系图里按顺序分配的序号,如果这个 module id 发生了变化,那么他的 chunkhash 也会发生变化。
这样会导致:如果你引入一个新的模块,会导致 module id 整体发生改变,可能会导致所有文件的chunkhash发生变化,这显然不是我们想要的
这里需要用 HashedModuleIdsPlugin ,根据模块的相对路径生成一个四位数的hash作为模块id,这样就算引入了新的模块,也不会影响 module id 的值,只要模块的路径不改变的话。
vue-cli /webpack.prod.conf.js
new webpack.HashedModuleIdsPlugin()
完成目标
至此如果我们改了某个模块的代码,是不会破坏其他模块的缓存,这就是我们想要实现的持久性缓存。
改造vue-cli中的webpack提升首页加载速度
分析
我们首先来看一个实际项目
运行 npm run build --report 可以查看打包分布图
我们发现最大的文件还是vendor,大部分框架代码都打包在这里面,而这些框架代码是不常变化的,也不需要每次进行打包。所以我们可以想办法把他们提取出来,挂到cdn上面去。
具体步骤
以 vue, vue-router,element-ui为例
步骤1 index.html cdn引入框架
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>demo-vue-project</title>
<link rel="stylesheet" href="https://cdn.bootcss.com/element-ui/2.0.8/theme-chalk/index.css">
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<script src="https://cdn.bootcss.com/vue/2.5.13/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/2.7.0/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/element-ui/2.0.7/index.js"></script>
</body>
</html>
步骤2 修改 build/webpack.base.conf.js
module.exports = {
...
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'element-ui': 'ELEMENT'
},
...
}
步骤3 修改框架注册方式
修改 src/router/index.js
// import Vue from 'vue'
import VueRouter from 'vue-router'
// 注释掉
// Vue.use(VueRouter)
...
修改 src/main.js
import Vue from 'vue'
import App from './App'
import router from './router'
import ELEMENT from 'element-ui'
// import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
Vue.use(ELEMENT)
Vue.prototype.$http = axios
/* eslint-disable no-new */
new Vue({
el: '#app',
store,
template: '<App/>',
components: { App }
})
打包
打包后我们发现vendor体积大大减小,因为库代码都用cdn加载了。但是这样会导致请求资源增多也有响应的代价,这仅可算是一个思路。 首屏问题的最终解决方案还是SSR
总结
至此 vue-cli中的打包配置,也有一些了解了。个人吐槽下webpack是真的复杂。观望和期待 parcel能来带不一样的体验。
转载自:https://juejin.cn/post/6844903540461142029