升级 webpack4 已经有一段时间了,将升级过程中记录下来。此次升级是基于 vue-cli 做的。
升级
将 Webpack 升级到 4.x 之后,直接运行webpack -xx,会提示还需要安装 webpack-cli。
The CLI moved into a separate package: webpack-cli. Please install webpack-cli in addition to webpack itself to use the CLI.
npm i webpack@latest webapck-cli -D
接下来升级所有的依赖,查看所有可以更新的包,进行升级。
npm outdate

默认配置
mode: 'development'
- 自动通过 DefinePlugin 设 process.env.NODE_ENV 的值为 development
- 自动开启 NamedChunksPlugin(固定 chunk id) 和 NamedModulesPlugin(开启 HMR 的时候使用该插件会显示模块的相对路径)
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
mode: 'production'
- 自动通过 DefinePlugin 设 process.env.NODE_ENV 的值为 production
- 自动开启 FlagDependencyUsagePlugin,FlagIncludedChunksPlugin,ModuleConcatenationPlugin,NoEmitOnErrorsPlugin,OccurrenceOrderPlugin,SideEffectsFlagPlugin,TerserPlugin
module.exports = {
+ mode: 'production',
- plugins: [
- new UglifyJsPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}
DefinePlugin 允许创建一个在编译时可以配置的全局常量。 在此处设置可以全局访问的环境变量 process.env.NODE_ENV,以便做相应不同的操作 。
所谓的“零配置”就是通过设置mode,自动配置两个环境通常需要做的操作。
mini-css-extract-plugin
webpack4 更推荐使用 mini-css-extract-plugin 替代 extract-text-webpack-plugin。该插件将CSS提取到单独的文件中。它为每个包含CSS的JS文件分离出一个CSS文件,并且支持CSS和SourceMap的按需加载。
css 原本是内联在 js 文件中的,之所以要将 css 独立拆包,是为了让 css 和 js 的改动互不影响,比如改了css,只会让 css 的缓存失效,而不会让 js 的缓存失效。并且它还会根据 optimization.splitChunks 的配置来自动拆包,比如将 element-ui 单独拆分出来作为一个 bundle, 那么 element-ui 的 css 也被单独拆分出来,而不是像原来将所有 css 合并成一个文件,动辄几百 KB 的大小。
与 extract-text-webpack-plugin 相比:
- Async loading [异步加载]
- No duplicate compilation (performance) [没有重复编译(性能)]
- Easier to use [更容易使用]
- Specific to CSS [专门为 css 设计]
// webpack.prod.conf.js
module.exports = merge(baseWebpackConfig, {
plugins: [
- new MiniCssExtractPlugin({
- filename: utils.assetsPath('css/[name].[contenthash].css')
- }),
]
}
// utils.js
// Extract CSS when that option is specified
// (which is the case during production build)
- if (options.extract) {
- return ExtractTextPlugin.extract({
- use: loaders,
- fallback: 'vue-style-loader',
- publicPath: '../../'
- })
- } else {
- return ['vue-style-loader'].concat(loaders)
- }
+ return [
+ options.extract ? {
+ loader: MiniCssExtractPlugin.loader,
+ options: {
+ publicPath: '../../'
+ }
+ } : 'vue-style-loader',
+ ].concat(loaders)
vue-loader
将 vue-loader 升级到15.x之后,需要增加如下代码:
const { VueLoaderPlugin } = require('vue-loader')
plugins: [
new VueLoaderPlugin(),
]
optimization
下面是 webpack4 默认的 optimization 配置。
- mode: development
+ mode: 'development'
- optimization: {
- namedModules: true,
- namedChunks: true,
- nodeEnv: 'development',
- flagIncludedChunks: false,
- occurrenceOrder: false,
- sideEffects: false,
- usedExports: false,
- concatenateModules: false,
- splitChunks: {
- hidePathInfo: false,
- minSize: 10000,
- maxAsyncRequests: Infinity,
- maxInitialRequests: Infinity,
- },
- noEmitOnErrors: false,
- checkWasmTypes: false,
- minimize: false,
- removeAvailableModules: false
- },
- mode: production
+ mode: 'production',
- optimization: {
- namedModules: false,
- namedChunks: false,
- nodeEnv: 'production',
- flagIncludedChunks: true,
- occurrenceOrder: true,
- sideEffects: true,
- usedExports: true,
- concatenateModules: true,
- splitChunks: {
- hidePathInfo: true,
- minSize: 30000, // 压缩前 chunk 的体积大于等于300000 byte
- maxAsyncRequests: 5, // 按需加载 bundle 的并发请求数小于等于5
- maxInitialRequests: 3, // 页面初始加载 bundle 的并发请求数小于等于3
- },
- noEmitOnErrors: true,
- checkWasmTypes: true,
- minimize: true,
- },
splitChunk
webpack4 其中一个重要改变就是 optimization.splitChunk 替代原先的 webpack.optimize.CommonsChunkPlugin。
只要设置 mode: production,webpack4 就会自动开启 splitChunk 进行 Code Splitting。默认的 splitChunk 的配置如上。
Code Splitting 的目的就是为了更好的利用浏览器缓存。虽然 webpack4 默认的拆包已经很不错,但是根据自己项目的情况还是可以进行优化。
基础库 vendor
如 vue,vuex,vue-router,axios 等第三方库,是项目构成的基础,每个页面都有用到,并且升级频率不高,除了组件库之外还有一些体积不大的第三方库,这些都放在这个包里。
组件库
项目中使用的组件库是 element-ui,由于几乎所有的组件都有用到,所以没有做按需引入。体积很大,没 Gzip 之前有658.86 KB,Gzip 之后也有160多 KB。比整个 vendor 的体积都大,并且更新频率也比 vendor 里的高。如果不将 element -ui 分割出来,一旦更新,那么整个 vendor + element-ui 体积大小的 bundle 要重新从服务器获取资源,不能利用缓存。因此将组件库从 vendor 中分割了出来。
业务代码
我们平时写的业务代码。项目中使用了路由懒加载,通过 component: () => import(...) 引入路由组件,webpack 就会根据路由自动进行代码分割。
manifest
我们还需要将存有 chunk 之间映射关系的 runtime 代码提取出来,否则它将包含在 bundle 之中。manifest 描述了 webpack 应该加载哪些文件,提取出来可以更快的加载文件而不用等待 bundle 加载。并且,哈希值是根据 chunk 内容生成的,一旦哈希值改变,runtime 代码也会改变,那么runtime 所在的 bundle 也将缓存失效。
inline-manifest-webpack-plugin
manifest 提出出来得到的文件通常很小,并且这个文件每次上线都会改变,需要为了几 KB 浪费一次请求数。因此更推荐将 manifest 内联到 html 文件中,毕竟 html 文件 是每次都会改变的。使用的插件是 inline-manifest-webpack-plugin,具体用法见链接,这里就不再赘述。
根据上面所说的我们可以做出如下配置
runtimeChunk: {
name: 'manifest',
},
splitChunks: {
chunks: 'all',
cacheGroups: {
// 抽离第三方插件
vendor: {
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
name: 'vendor',
},
'vendor-element-ui': {
name: 'vendor-element-ui', // split elementUI into a single package
priority: 20, // the weight needs to be larger than vendor and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
enforce: true,
},
},
},
minimizer
optimize-css-assets-webpack-plugin
将 css 分离出来后,还需要对 css 做压缩与优化。这是为什么还需要optimize-css-assets-webpack-plugin。
optimization: {
minimizer: [new OptimizeCSSAssetsPlugin()]
}
optimize-css-assets-webpack-plugin 默认使用 cssnano 来做 css 优化,压缩代码,删掉无用的注释,去掉冗余的代码,还会优化代码及书写顺序。因此大大减少了 css 文件的大小。
terser-webpack-plugin
在 mode: production 模式下,会自动开启 minimize: true,自动压缩代码,内置的插件就是 terser-webpack-plugin,但是 terser-webpack-plugin 的默认配置并不能满足我们的项目。
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
drop_console: true,
safari10: true,
},
sourceMap: config.build.productionSourceMap,
parallel: true,
cache: true,
}),
]
}
打包速度对比
版本 | 第一次 | 第二次 | 第三次 | 第四次 | 第五次 |
---|---|---|---|---|---|
webpack3 | 49324 | 52749 | 49431 | 51893 | 49725 |
webpack4 | 39034 | 33620 | 31968 | 30814 | 31874 |
可以看到打包速度有了质的飞跃,大幅降低上线所需要的时间。