共计 7510 个字符,预计需要花费 19 分钟才能阅读完成。
转眼已经是 2019 年,短短三四年时间,webpack 打包工具成为了前端开发中必备工具,曾经一度的面试题都是问,请问前端页面优化的方式有哪些?大家也是能够信手拈来的说出缓存、压缩文件、CSS 雪碧图以及部署 CDN 等等各种方法,但是今天不一样了,可能你去面试问的就是,请问你是否知道 webpack 的打包原理,webpack 的打包优化方法有哪些?所以该说不说的,笔者闲着没事研究了一下 webpack 的打包优化,可能大家都有看过类似的优化文章~ 但是笔者还是希望能够给大家一些新的启发~
1、准备工作:测速与分析 bundle
既然我们要优化 webpack 打包,肯定要提前对我们的 bundle 文件进行分析,分析各模块的大小,以及分析打包时间的耗时主要是在哪里,这里主要需要用到两个 webpack 插件,speed-measure-webpack-plugin 和 webpack-bundle-analyzer,前者用于测速,后者用于分析 bundle 文件。
具体配置
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const smp = new SpeedMeasurePlugin({outputFormat:"human",});
module.exports = {
configureWebpack: smp.wrap({
plugins: [
new webpack.ProvidePlugin({
$: "zepto",
Zepto: "zepto",
}),
new BundleAnalyzerPlugin(),],
optimization: {
splitChunks: {
cacheGroups: {
echarts: {
name: "chunk-echarts",
test: /[\\/]node_modules[\\/]echarts[\\/]/,
chunks: "all",
priority: 10,
reuseExistingChunk: true,
enforce: true,
},
demo: {
name: "chunk-demo",
test: /[\\/]src[\\/]views[\\/]demo[\\/]/,
chunks: "all",
priority: 20,
reuseExistingChunk: true,
enforce: true,
},
page: {
name: "chunk-page",
test: /[\\/]src[\\/]/,
chunks: "all",
priority: 10,
reuseExistingChunk: true,
enforce: true,
},
vendors: {
name: "chunk-vendors",
test: /[\\/]node_modules[\\/]/,
chunks: "all",
priority: 5,
reuseExistingChunk: true,
enforce: true,
},
},
},
},
})
}
由于是基于 vue-cli 脚手架的,所以其实 vue-cli 中已经帮你做了一些优化的工作,可以看到,原先项目最初的配置设置了 splitchunk,进行代码分割,这在大型项目中是很有必要的,毕竟你不希望你的用户阻塞加载一个 5MB 大小的 JS 文件,所以做代码分割和懒加载是很有必要的。
说远了,我们来看看这个配置,你需要用 smp 对配置进行再包裹,因为 SpeedMeasurePlugin 会对你的其他 Plugin 对象包裹一层代理,这样的目的是为了能够知道 plugin 开始和结束的时间~
其次,BundleAnalyzerPlugin 就跟普通的 plugin 一样,加载 plugins 数组的后面即可。
接下来我们看一下最初的打包时间以及包内容分析:
可以看到项目中较大的三个包,其中两个包是我们的第三方依赖,three.js、lottie、lodash、echarts 等。
2、开始逐步优化
2.1 缩小文件查找和处理范围
这是 webpack 优化中的常规操作,基本就是对模块和文件查找的优化,以及减少 loader 对一些不必要模块的处理,但是 vue-cli 中的 loader 并没有暴露给我们操作,所以其内置的 loader 处理无法由我们进行优化,但是其实 vue-cli 中的配置项已经对 loader 的查找路径进行了优化,如果你的项目也是使用了 vue-cli,你可以通过以下命令行查看你现有的配置文件是怎样的:
npx vue-cli-service inspect > output.js
具体可以翻阅 vuecli 官方文档。
resolve:{modules: [path.resolve(__dirname, 'node_modules')],
alias:{'three':path.resolve(__dirname, './node_modules/three/build/three.min.js'),
'zepto$':path.resolve(__dirname, './node_modules/zepto/dist/zepto.min.js'),
'swiper$':path.resolve(__dirname, './node_modules/swiper/dist/js/swiper.min.js'),
'lottie-web$':path.resolve(__dirname, './node_modules/lottie-web/build/player/lottie.min.js'),
'lodash$':path.resolve(__dirname, './node_modules/lodash/lodash.min.js'),
}
},
module:{noParse:/^(vue|vue-router|vuex|vuex-router-sync|three|zepto|swiper|lottie-web|lodash)$/
},
- 通过 modules 指定查找第三方模块的路径。
- 通过 alias 指定第三方模块直接查找到打包构建好的压缩 js 文件。
- 通过 module 指定 noparse,对第三方模块不再进行分析依赖。
优化效果:2s?
可以看到时间就减少了两三秒,在 30s 波动,感觉没有多大差别。
2.2 尝试使用 happypack
由于在进行 webpack 优化前,翻阅了很多有关 webapck 优化的文章,所以笔者也想尝试一下用 happypack 来优化打包时间。
在想要用 happypack 进行的打包之前,大抵有这两种说法:
1、webpack4 中已经默认是多线程打包了,所以 happypack 打包效果不明显;
2、vue 不支持 happypack 打包,需要设置 thread-loader。
但是笔者想了一下,还是试试看把,大不了我只对 JS 和 CSS 文件设置 happypack。
但是问题又来了,vue-cli 内置封装了 loader,这个时候我要怎么拿到它的配置,改写里面的 loader 配置呢。
通过翻阅 vue-cli 的官方文档我们可以看到以下使用介绍:
configureWebpack
Type: Object | Function
如果这个值是一个对象,则会通过 webpack-merge 合并到最终的配置中。如果这个值是一个函数,则会接收被解析的配置作为参数。该函数及可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本。
为此,笔者特地调试进了 vue-cli 的源码一探究竟:
流程介绍:
由于我们执行命令行 vue-cli-service build,其实是先去 node_modules 的.bin 文件夹下查找相应的可执行文件,.bin 下的 vue-cli-service 会映射到相应的第三方库内的执行文件。
所以我们可以找到这个可执行文件的地址:
/node_modules/@vue/cli-service/bin/vue-cli-service.js
找到了入口,接下来我们想要进入 nodejs 的调试,在以往的开发中,我们会通过 node –inspect app.js 的方式启动一个后台服务,然后在谷歌浏览器里进入调试界面(F12 选择绿色的那个小按钮)
但是这里却犯了难,由于我们的打包构建是一次执行的,不同于一个后台服务,是实时监听的,服务一直启动着。查阅了一下,如果是普通的 nodejs 文件想要调试的话,需要通过这样的方式:
node --inspect-brk=9229 app.js
所以,为了强行走进去 vue-cli 的源码进行调试,可看 vue-cli 的处理流程,我们需要这样输入以下命令行:
node --inspect-brk=9229 node_modules/@vue/cli-service/bin/vue-cli-service.js build
上面的这个命令行,等价于 vue-cli-service build。
通过这样的方式,我们终于走进了 vue-cli 的源码,看了它的执行流程,你可以在对应的位置打下断点,查看此时的作用域内的变量数据。
可以看到 vue-cli 源码里的这一段操作,会执行我们传入的函数,判断函数有没有返回值来决定是否要 merge 进其内部配置的 config。
通过这段代码我们可以看出,如果我们 configWepack 配置为函数,之后通过参数的形式获取到 config 配置项,本身是一个对象,对象是保留引用的形式,所以如果我们直接对传入的 config 对象进行修改,就可以实现我们最初的目标!修改 vue-cli 内置的 loader!
当然,除了断点进入里面看配置,刚才也说了,我们可以通过命令行输出为一个 output 文件查看现有的配置。
这里可以给大家截图看一下 vue-cli 内部的配置:
可能有点废话了,但是通过断点的方式,我们可以看到 vue-cli 其实已经对 js 文件设置了 exclude,同时也帮我们设置好了 cache-loader,意味着 webpack 常规的优化方式之一,使用 cache-loader 缓存它也帮我们做了。
回到最初的起点,我们想要处理的是针对 JS 和 CSS 的 loader,于是模仿大多数的配置,我进行了以下修改:
configureWebpack:(config)=>{console.log("webpack config start");
let originCssRuleLoader = config.module.rules[6].oneOf[0].use;
let newCssRuleLoader = 'happypack/loader?id=css';
config.module.rules[6].oneOf[0].use = newCssRuleLoader
config.module.rules[6].oneOf[1].use = newCssRuleLoader
config.module.rules[6].oneOf[2].use = newCssRuleLoader
config.module.rules[6].oneOf[3].use = newCssRuleLoader
...//other code
}
尝试对 css 的 loader 配置进行修改。之后对 plugins 进行一下配置:
plugins: [
new HappyPack({
id: 'css',
threads: 4,
loaders: originCssRuleLoader
}),
],
本以为这样就 OK 了,但是很遗憾的告诉大家,报错了 …
可以看到报错的内容,是在处理 vue 文件的时候,出了错误。
如何解决
笔者百度了,也谷歌了,大抵是说 happypack 不支持 vue-loader,同时,根据报错也查了一下处理的方案,通过设置 parallel 参数,也还是无效。
笔者甚至怀疑是自己的 happypack 配置不对,于是我把配置原样移植配置到另一个非 vue 项目中,一切运行正常。
答案:此题无解~
原因分析:
由于 vue 文件中会含有 CSS,所以 vue-loader 会提取出其中的 css,交给其他 loader 处理,vue-loader-plugin 会通过在 vue 文件后面加上查询字符串来告诉其他 loader,针对这个文件要做处理。意味着什么呢?我们的 vue-loader 在处理文件的时候,通知其他 loader 处理,但是此时的 loader 配置已经被我们改写成了 happypack,而 vue 又与 happypack 不兼容,最终导致了报错。很遗憾的告诉大家,vue-cli 接入 happypack– 失败。
(注:这一部分主要是笔者在 webpack 优化过程中的探索,虽然最终不能让自己的 webpack 打包很好的优化,但是在这个探索的过程中,我们也可以学到很多~ 包括 vue-cli 对配置对象的处理?如何调试普通文件 nodejs 代码?vue-loader 中对 vue 文件的处理流程?vue-loader-plugin 帮我们做了什么事?而这些都是要自己慢慢翻阅,慢慢踩坑去了解的~)
2.3 使用 dllplugin
和大多数的 webpack 优化教程一样,笔者也尝试了利用 dllplugin 进行优化,该插件的本质,是提取出我们常用的第三方模块,单独打成一个文件包,之后插入到我们的 html 页面中,这样我们以后每次打包,都不需要针对第三方模块进行处理,毕竟第三方模块动辄成千上万行。
流程介绍:
- 1、配置 webpack.dll.js 针对第三方库打包
- 2、vue.config.js 中配置 plugin
- 3、html 中引入 dll 打包出来的 js 文件。(一般采用部署 CDN 的方式)
由于项目中有很多大型的第三方库,类似 three、echart 等,所以笔者进行了以下配置:(webpack.dll.js)
const webpack = require("webpack")
const path = require("path")
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
vuebundle: [
'vue',
'vue-router',
'vuex',
],
utils:[
'lodash',
'swiper',
'lottie-web',
'three',
],
echarts:[
'echarts/lib/echarts',
"echarts/lib/chart/bar",
"echarts/lib/chart/line",
"echarts/lib/component/tooltip",
"echarts/lib/component/title",
"echarts/lib/component/legend",
]
},
output: {path: path.resolve(__dirname, './static/'),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({path: path.join(__dirname, 'build', '[name]-manifest.json'),
name: '[name]_library'
})
]
}
针对不同的库的大小进行划分,打了三个包,为啥不打成一个包?一个包那就太大了,你并不希望你的用户加载一个大型 JS 文件包而阻塞,影响页面性能。
接下里是 vue.config.js 的配置:
plugins: [
new webpack.ProvidePlugin({
$: "zepto",
Zepto: "zepto",
}),
new DllReferencePlugin({manifest: require('./build/echarts-manifest.json'),
}),
new DllReferencePlugin({manifest: require('./build/utils-manifest.json'),
}),
new DllReferencePlugin({manifest: require('./build/vuebundle-manifest.json'),
}),
new BundleAnalyzerPlugin(),]
引入了 DllPlugin。接下来配置 HTML:
(由于笔者没将 DLL 打包出来的 js 文件上传到 CDN,所以只能本地自己起个 node 服务器返回静态资源了)
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<script type="text/javascript" src="http://localhost:3000/echarts.dll.js"></script>
<script type="text/javascript" src="http://localhost:3000/utils.dll.js"></script>
<script type="text/javascript" src="http://localhost:3000/vuebundle.dll.js"></script>
</body>
然后 npm run serve,开始页面调试和开发~
舒服~
优化结果:
由于少了大型第三方库,所以时间控制在了 20s 左右了。优化相对比较明显~
3、优化与探索总结
优化到这,基本就结束了。
webpack 常见的优化方式,优化路径查找、设置缓存、happypack 以及 dllplugin,前两项 vue-cli 已经帮我们做了一些,而 happypack 由于不和 vue 兼容,导致无法接入,dllplugin 通过单独提取第三方库,取得了明显优化。
当然,笔者也尝试剔除了一些项目中无用的代码,不过也是不痛不痒。
webpack 优化方式总结:
- 1、优化模块查找路径
- 2、剔除不必要的无用的模块
- 3、设置缓存:缓存 loader 的执行结果(cacheDirectory/cache-loader)
- 4、设置多线程:HappyPack/thread-loader
- 5、dllplugin 提取第三方库
当然,这是针对开发的优化,如果是针对部署上的优化呢?我们可以设置 splitchunk、按需加载、部署 CDN 等,这里就不展开了。
最后
希望这篇文章能够大家有所收获~ webpack 已经是前端仔必备技能了~ 有空大家钻研一下 webpack 的配置和原理,也是会有所收获的!谢谢观看~