Webpack4学习笔记(二)——代码分割(单入口)
这是一个关于Webpack4的文章系列,其中包含以下文章:
- Webpack4学习笔记(一)——基本使用
- Webpack4学习笔记(二)——代码分割(单入口)
- Webpack4学习笔记(三)——代码分割(多入口)
- Webpack4学习笔记(四)——CSS处理
- Webpack4学习笔记(五)——分离production和development环境
- Webpack4学习笔记(六)——Webpack4+VueJS2项目搭建
本系列文章源代码:
git clone https://github.com/davenkin/webpack-learning.git
在上一篇文章中,我们讲到了Webpack4的基本使用,本文将讲到单入口项目的代码分割。
欢迎访问本文github源代码。
Webpack会在以下三种情况下对代码进行分割:
- 入口文件,每个入口文件将有单独的一次代码分割;
- 使用
SplitChunksPlugin
插件; - 异步加载,比如使用
import()
。
代码分割(Code Split)主要出于两种原因:一是减少代码重复,比如在多页面的应用中,如果多个页面都同时引用了某些module的情况;二是支持缓存,比如第三方库通常是不会怎么变的,将他们单独抽离出来有利于浏览器缓存。
具体来讲,在做分割时,我们通常会针对以下情况进行处理:
- 为第三方依赖库(vendor)单独打包
- 为webpack自己的runtime代码(manifest)单独打包
- 为公共业务代码单独打包
- 为异步加载的module单独打包
Webpack4默认分割配置
Webpack4放弃了CommonsChunkPlugin,转而使用SplitChunksPlugin,并通过内置的optimization配置段进行配置,production模式下默认配置请参考该插件的官方文档,这里列出重要的几项:
- 默认只对两种情况进行分割:一是异步加载的module,二是被其他chunk引用次数大于等于2的module
- 默认生产chunk最小为30k
- 默认有两个cacheGroup,一个为vendors用于处理第三方依赖库;一个是default(处理当module被引用等于或超过2次时情况)
由于一个module有可能同属于多个cacheGroup,因此可以通过设置某个cacheGroup的优先级(priority)来解决,priority值越大,表示优先级越高,也即会优先其作用。Webpack的两个默认cacheGroup的优先级都被设置成了负数,而我们自定义的cacheGroup的默认priority为0,因此可以初步保证自定义的cacheGroup总会优先于默认的起作用。
Webpack默认配置下的代码分割
为了模拟真实项目,创建项目结构如下:
- index.js依赖于A-module,axios库以及mathjs库,并异步依赖于lodash库(async-lodash)和underscore库(async-underscore)
- index.js异步依赖于B-module(async-b)和C-module(async-c)
- B-module和C-module都依赖于D-module
- C-module还依赖于E-module
在默认配置下,运行cnpm run build
,得到输出如下:
distribution/
├── async-b.a6a6717b3a9e94564a8f.js
├── async-c.cad9d85f1f375c3b56d7.js
├── bundle-analyzer-report.html
├── index.html
├── main.fa1c3667353182bc989d.js
├── vendors~async-lodash.d98165c58c58f225c6fd.js
└── vendors~async-underscore.44ac919350197fdd72c7.js
可以看出,B-module和C-module由于是index.js异步引入的,因此分别为其创建了一个输出文件(async-b.a6a6717b3a9e94564a8f.js
和async-c.cad9d85f1f375c3b56d7.js
);对于异步引入的lodash和underscore而言,由于来自于第三方库,webpack对第三方库有默认的配置(配置有名为vendors的cacheGroup),因此webpakc会为每个第三方库单独默认生成对应的异步加载文件(vendors~async-lodash.d98165c58c58f225c6fd.js
和vendors~async-underscore.44ac919350197fdd72c7.js
)。main.fa1c3667353182bc989d.js
中包含了所有非异步加载的模块,包含了第三方库axios和MathJS,以及我们自己的A-module。生成的bundler报告(bundle-analyzer-report.html
)如下:
定制化代码分割
以上在默认配置下生成的bundle有以下缺点:
- webpack本身的runtime代码没有分离出来
- main.fa1c3667353182bc989d.js既包含了第三方库,有包含了我们自己的代码
- B-module和C-module同时依赖了D-module,但是D-module在B-module和C-module中重复存在
- webpack默认通过数字编号给module起名,如果module发生增减,会导致包含第三方库在内的bundle都会发生改变
对于第1点,可以通过webpack自带的runtimeChunk
配置解决:
...
optimization: {
runtimeChunk: {
"name": "manifest"
},
...
此时,webpack会为runtime代码单独生成名为manifest-*.js
的文件。
对于第2点,我们需要将所有的第三方库抽离出来。前文讲到,webpack默认情况下只会对异步加载的第三方库进行分割,此时我们需要修改一下配置:
...
splitChunks: {
chunks:'all'
}
...
将chunks值改为all(先前的默认值为async),表示对所有的第三方库进行代码分割(包括async和initial)。此时生产的代码中多了一个vendors~main.*.js
文件。(adding optimization.splitChunks.chunks = 'all' is a way of saying “put everything in node_modules into a file called vendors~main.js".)
对于第3点,需要新加一个cacheGroup,该cacheGroup对于被引用
此时大于等于2的module进行分割:
...
cacheGroups: {
common: {
minChunks: 2,
name:'commons',
chunks: 'async',
priority: 10,
reuseExistingChunk: true,
enforce: true
}
}
...
在以上配置中,minChunks:2
表示被引用次数大于等于2的module符合该cacheGroup的条件;name:'commons'
表示将所有符合条件的module都放到同一个名为commons的文件中,如果不配置此项,webapck会默认根据module被引用情况生成多个bundle文件,webpack官网其实并不建议设置name属性,原文如下:
When assigning equal names to different split chunks, all vendor modules are placed into a single shared chunk, though it's not recommend since it can result in more code downloaded.
chunks: 'async',
表示通过异步加载的module符合条件,这里笔者有些不解:B-module和C-module虽然本身是通过async引入的,但是他们对D-module的引用是通过initial的方式引入的,因此按理应该设置成chunks: 'initial‘
才对,然而事实上这里必须设置成async,D-module才会独立出来。
priority: 10,
表示优先处理,因为webpack默认的两个cacheGroup的优先级为负数。
此时,运行cnpm start build
的输出结果中多了一个commons.*.js
文件。
对于第4点,加入HashedModuleIdsPlugin插件即可:
...
const webpack = require('webpack');
...
new webpack.HashedModuleIdsPlugin()
...
综合以上,对于单入口项目来说,建议配置如下:
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
function resolve(dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'distribution')
},
//use inline-source-map for development:
devtool: 'inline-source-map',
//use source-map for production:
// devtool: 'source-map',
devServer: {
contentBase: './distribution'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src')],
exclude: [resolve('node_modules')]
}
]
},
plugins: [
new CleanWebpackPlugin(['distribution']),
new BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: 'bundle-analyzer-report.html'
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new webpack.HashedModuleIdsPlugin()
],
optimization: {
runtimeChunk: {
"name": "manifest"
},
splitChunks: {
chunks: 'all',
cacheGroups: {
common: {
minChunks: 2,
name: 'commons',
chunks: 'async',
priority: 10,
reuseExistingChunk: true,
enforce: true
}
}
}
}
}
;
运行cnpm run build
输出如下:
distribution/
├── async-b.e367719620c9ca8ce61f.js
├── async-c.ed6ccbf81ee739e1680d.js
├── bundle-analyzer-report.html
├── commons.69d2ac742d5253325cb1.js
├── index.html
├── main.d144c2daa62efff6dcc9.js
├── manifest.20a9103ae59dd6003f45.js
├── vendors~async-lodash.d810246d214be5df29ec.js
├── vendors~async-underscore.7ca24e0a09ca4a78e339.js
└── vendors~main.5016e1c2cb426d8b4eef.js
生产的bundle报告(bundle-analyzer-report.html
)如下:
在下一篇文章中,我们将讲到多入口项目的代码分割。