Fork me on GitHub

webpack4笔记(3)-代码分割

为什么要代码分割?

  1. 减少代码重复,比如在多页面的应用中,如果多个页面都同时引用了某些module的情况;
  2. 支持缓存,比如第三方库通常是不会怎么变的,将他们单独抽离出来有利于浏览器缓存。

什么情况下代码分割?

  • 入口文件,每个入口文件将有单独的一次代码分割
  • 使用SplitChunksPlugin插件;
  • 异步加载,比如使用import()。

做代码分割时,通常会对以下情况进行处理:

  • 为第三方依赖库(vendor)单独打包
  • 为webpack自己的runtime代码(manifest)单独打包
  • 为公共业务代码单独打包

webpack4默认分割配置

Webpack4放弃了CommonsChunkPlugin,使用SplitChunksPlugin,并通过内置的optimization配置段进行配置。详情在 官方文档,重点的几项:

  • 默认只对两种情况进行分割:一是异步加载的module,二是被其他chunk引用次数大于等于2的module。
  • 默认生产chunk最小为30k
  • 默认有两个cacheGroup,一个vendors用于处理第三方依赖库;一个是default(处理当module被引用>=2的情况)

由于一个module有可能同属于多个cacheGroup,因此可以通过设置某个cacheGroup的优先级(priority)来解决,priority值越大,表示优先级越高,也即会优先其作用。

Webpack的两个默认cacheGroup的优先级都被设置成了负数,而我们自定义的cacheGroup的默认priority为0,因此可以初步保证自定义的cacheGroup总会优先于默认的起作用。

单入口代码分割

webpack默认配置下的代码分割

引入webpack-bundle-analyzer插件,打包文件分析工具

src文件夹下建立以下几个文件:

  1. index.js依赖于A-module,axios库以及MathJS库,异步依赖于lodash库(async-lodash)和underscore库(async-underscore),B-module(async-b)和C-module(async-c)
  2. B-module和C-module都依赖于D-module
  3. C-module依赖于E-module

执行 yarn build:

1
2
3
4
5
6
7
8
dist/
├── async-b.d524a562ae289f073b19.js
├── async-c.d524a562ae289f073b19.js
├── bundle-analyzer-report.html
├── index.html
├── main.d524a562ae289f073b19.js
├── vendors~async-lodash.d524a562ae289f073b19.js
└── vendors~async-underscore.d524a562ae289f073b19.js
  • B-module和C-module由于是index.js异步引入的,因此分别为其创建了一个输出文件
  • 对于异步引入的lodash和underscore而言,由于来自于第三方库,webpack对第三方库有默认的配置(配置有名为vendors的cacheGroup),因此webpakc会为每个第三方库单独默认生成对应的异步加载文件
  • main.d524a562ae289f073b19.js中包含了所有非异步加载的模块,包含了第三方库axios和MathJS,以及A-module。

定制代码分割

默认配置下生成的bundle存在缺点:

  1. webpack本身的runtime代码没有分离出来
  2. main.d524a562ae289f073b19.js既包含了第三方库,又包含了我们自己的代码
  3. B-module和C-module同时依赖了D-module,但是D-module在B-module和C-module中重复存在
  4. webpack默认通过数字编号给module起名,如果module发生增减,会导致包含第三方库在内的bundle都会发生改变

定制代码分割要针对以上问题作出处理:

  • 问题1:通过webpack自带的runtimeChunk配置解决runtime代码分离问题:
1
2
3
4
5
6
7
...
optimization: {
runtimeChunk: {
"name": "mainfest"
}
}
...

webpack会为runtime代码单独生成名为manifest-*.js的文件。

  • 问题2:需要将所有的第三方库从main.js中抽离出来。webpack默认情况下只会对异步加载的第三方库进行分割,此时我们需要修改一下配置:
1
2
3
spliteChunks: {
chunks: 'all'
}

将chunks值改为all(先前的默认值为async),表示对所有的第三方库进行代码分割(包括async和initial)。此时生产的代码中多了一个vendors~main.*.js文件。

  • 问题3:要新加一个cacheGroup,该cacheGroup对于被引用
    此时大于等于2的module进行分割:
1
2
3
4
5
6
7
8
9
10
cacheGroups: {
common: {
minChunks: 2,
name: 'commons',
chunks: 'async',
priority: 10,
reuseExistingChunk: true,
enforce: true
}
}

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.

参数说明:

参数 说明
minChunks: 2 表示被引用次数大于等于2的module符合该cacheGroup的条件
name:’commons’ 表示将所有符合条件的module都放到同一个名为commons的文件中,如果不配置此项,webapck会默认根据module被引用情况生成多个bundle文件
chunks: ‘async’ 表示通过异步加载的module符合条件,B-module和C-module虽然本身是通过async引入的,但是他们对D-module的引用是通过initial的方式引入的,因此按理应该设置成chunks: ‘initial‘才对,然而事实上这里必须设置成async,D-module才会独立出来。
priority: 10 表示优先处理,因为webpack默认的两个cacheGroup的优先级为负数。
  • module发生增减,会导致包含第三方库在内的bundle都会发生改变
    问题,加入HashedModuleIdsPlugin插件即可解决:
1
2
3
...
new webpack.HashedModuleIdsPlugin()
...

单入口项目建议配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
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;

module.exports = {
entry: './src/index.js',
output: {
filename: '[name].[hash].js',
chunkFilename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist')
},
//use inline-source-map for development:
devtool: 'inline-source-map',

//use source-map for production:
// devtool: 'source-map',
devServer: {
contentBase: './dist'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.resolve(__dirname, 'src')],
exclude: [path.resolve(__dirname, 'node_modules')]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new BundleAnalyzerPlugin({
openAnalyzer: true,
analyzerMode: 'static',
reportFilename: 'bundle-analyzer-report.html'
}),
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
// 解决 hash 频繁变动的问题
new webpack.HashedModuleIdsPlugin()
],
optimization: {
// 分割webpack运行时代码
runtimeChunk: {
name: 'manifest'
},
spliteChunks: {
chunks: 'all',
cacheGroups: {
common: {
// 最小引用次数
minChunks: 2,
// 符合条件的module都放到同一个名为commons的文件中
name: 'commons',
// 通过异步加载的module符合条件
chunks: 'async',
// 提升优先级
priority: 10,
// 使用已存在的
reuseExistingChunk: true,
// 强制执行
enforce: true
}
}
}
}
}

运行 yarn build 输出如下:

1
2
3
4
5
6
7
8
9
10
11
dist/
├── async-b.75f4ab95f64ececd9f8b.js
├── async-c.75f4ab95f64ececd9f8b.js
├── bundle-analyzer-report.html
├── commons.75f4ab95f64ececd9f8b.js
├── index.html
├── main.75f4ab95f64ececd9f8b.js
├── manifest.75f4ab95f64ececd9f8b.js
├── vendors~async-lodash.75f4ab95f64ececd9f8b.js
├── vendors~async-underscore.75f4ab95f64ececd9f8b.js
└── vendors~main.75f4ab95f64ececd9f8b.js

多入口代码分割

webpack默认配置下的代码分割

src文件夹下建立以下几个文件:

  1. 3个html入口文件,index1.html,index2.html,index3.html
  2. 每个入口文件对应一个js入口文件,index1.js,index2.js,index3.js
  3. index1.js依赖A-module、F-module、G-module、axios、lodash,异步加载B-module、C-module
  4. index2.js依赖A-module、G-module、H-module、axios、jquery,异步加载lodash、B-module
  5. index3.js依赖A-module、F-module、H-module、lodash、jquery,异步加载C-module
  6. B-module依赖于D-module
  7. C-module依赖于D-module、E-module

更改entry和HtmlWebpackPlugin配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
entry: {
'index1': './src/index1.js',
'index2': './src/index2.js',
'index3': './src/index3.js'
},
plugins: [
new CleanWebpackPlugin(['dist']),
new BundleAnalyzerPlugin({
openAnalyzer: false,
analyzerMode: 'static',
reportFilename: 'bundle-analyzer-report.html'
}),
new HtmlWebpackPlugin({
template: './src/index1.html',
filename: 'index1.html'
}),
new HtmlWebpackPlugin({
template: './src/index2.html',
filename: 'index2.html'
}),
new HtmlWebpackPlugin({
template: './src/index3.html',
filename: 'index3.html'
}),
new webpack.HashedModuleIdsPlugin()

]
...

运行 yarn build:

1
2
3
4
5
6
7
8
9
10
11
dist/
├── async-b.97a1d3ca8bef2a75acb1.js
├── async-c.97a1d3ca8bef2a75acb1.js
├── bundle-analyzer-report.html
├── index1.97a1d3ca8bef2a75acb1.js
├── index1.html
├── index2.97a1d3ca8bef2a75acb1.js
├── index2.html
├── index3.97a1d3ca8bef2a75acb1.js
├── index3.html
└── vendors~async-lodash.97a1d3ca8bef2a75acb1.js

webpack的默认配置:

  • 只对异步加载的moudule进行分割处理
  • 生成的分割文件要大于30k
  • 对异步加载的第三方库(node_modules目录下的库)进行处理
  • 对引用大于等于2的module进行分割

由上面的打包结果可以看出,在默认情况下,webpack只对异步加载的库做了分割处理(生成了async-b.3e6a567c8ad867591384.js、async-c.faa08dc1c722e8898326.js和vendors~async-lodash.d810246d214be5df29ec.js),而其他所有的module都打包到了对应的index.*.js文件中。

改进默认配置

可以对配置稍作修改,即将默认的两个cacheGroup的chunks改为all,表示同时对静态加载(initial)和动态加载(async)起作用,以及设置生产的chunk不受最大文件大小限制。

1
2
3
4
5
6
7
8
9
10
11
...
optimization: {
runtimeChunk: {
"name": "manifest"
},
splitChunks: {
chunks: 'all',
minSize: 0
}
}
...

运行 yarn build 输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dist/
├── async-b.6d7cd2350dd7e9f02e59.js
├── async-b~async-c.6d7cd2350dd7e9f02e59.js
├── async-c.6d7cd2350dd7e9f02e59.js
├── bundle-analyzer-report.html
├── index1.6d7cd2350dd7e9f02e59.js
├── index1.html
├── index2.6d7cd2350dd7e9f02e59.js
├── index2.html
├── index3.6d7cd2350dd7e9f02e59.js
├── index3.html
├── manifest.6d7cd2350dd7e9f02e59.js
├── vendors~async-lodash~index1~index3.6d7cd2350dd7e9f02e59.js
├── vendors~index1~index2.6d7cd2350dd7e9f02e59.js
└── vendors~index2~index3.6d7cd2350dd7e9f02e59.js
  • async-b~async-c.6d7cd2350dd7e9f02e59.js文件是新产生的,由于B-module和C-module同时依赖于D-module产生
  • vendors~async-lodash~index1~index3.6d7cd2350dd7e9f02e59.js含了lodash库,是由于index1.js、index3.js静态依赖了lodash,同时index2.js中对lodash有异步依赖;
  • vendors~index1~index2.6d7cd2350dd7e9f02e59.js包含了axios库,是由于index1和index2同时引用了axios;
  • vendors~index2~index3.6d7cd2350dd7e9f02e59.js包含了jquery库,是由于index2和index3同时引用了jquery。

此时仍然存在以下几个问题:

  • 通过HtmlWebpackPlugin生成的三个index.html文件引用了所有的静态依赖,没有达到分离页面的目的;
  • 三个index.js文件共享或者部分共享了A-module、F-module、G-module和H-module,但是共享模块还是重复出现在了生成的index.js文件中。

想要达到的目的:

  1. 提取webpack的runtime代码到单独的文件
  2. 所有静态依赖第三方库被分割到同一个文件中
  3. 所有动态依赖的第三方库分别分割到单独的文件,这样才能享受异步加载的好处,即只加载所需要的
  4. 所有动态依赖的自研模块分别分割到单独的文件
  5. 被多次引用的自研发模块统一放到一个文件中,便于多个入口共享
  6. 配置缓存

定制代码分割

先去除掉默认的2个cacheGroup:

1
2
3
4
5
6
7
8
optimization: {
splitChunks: {
cacheGroups: {
default: false,
vendors: false
}
}
}

运行 yarn build,输出:

1
2
3
4
5
6
7
8
9
10
11
12
dist/
├── async-b.c25a8aa49f1a66c2f974.js
├── async-c.c25a8aa49f1a66c2f974.js
├── async-lodash.c25a8aa49f1a66c2f974.js
├── bundle-analyzer-report.html
├── index1.c25a8aa49f1a66c2f974.js
├── index1.html
├── index2.c25a8aa49f1a66c2f974.js
├── index2.html
├── index3.c25a8aa49f1a66c2f974.js
├── index3.html
└── manifest.c25a8aa49f1a66c2f974.js
  1. 提取webpack的runtime代码
1
2
3
4
5
6
...
optimization: {
runtimeChunk: {
"name": "manifest"
},
...

2.将所有静态的第三方依赖放到同一个文件中,加入新的cacheGroup:

1
2
3
4
5
6
7
8
9
...
vendor: {
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
enforce: true,
priority: 10,
name:'vendor'
},
...

yarn build 输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
dist/
├── async-b.68aff42cd15644abcd53.js
├── async-c.68aff42cd15644abcd53.js
├── async-lodash.68aff42cd15644abcd53.js
├── bundle-analyzer-report.html
├── index1.68aff42cd15644abcd53.js
├── index1.html
├── index2.68aff42cd15644abcd53.js
├── index2.html
├── index3.68aff42cd15644abcd53.js
├── index3.html
├── manifest.68aff42cd15644abcd53.js
└── vendor.68aff42cd15644abcd53.js

多了一个vendor.68aff42cd15644abcd53.js,包含所有共享的三方依赖(jquey,lodash和axios)

如果去掉 name: ‘vendor’,得到的输出为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dist/
├── async-b.3c89aa6533d013e845c9.js
├── async-c.f895770163eede087e4b.js
├── async-c.3c89aa6533d013e845c9.js
├── bundle-analyzer-report.html
├── index1.3c89aa6533d013e845c9.js
├── index1.html
├── index2.3c89aa6533d013e845c9.js
├── index2.html
├── index3.3c89aa6533d013e845c9.js
├── index3.html
├── manifest.3c89aa6533d013e845c9.js
├── vendor~index1~index2.3c89aa6533d013e845c9.js
├── vendor~index1~index3.3c89aa6533d013e845c9.js
└── vendor~index2~index3.3c89aa6533d013e845c9.js

此时webpack会根据实际模块的共享关系,分开生成精确的chunk文件,比如如果A和B同时依赖于C,那么webpack会专门为A和B生成对应的chunk文件。

对比发现配置name属性后,所有的依赖都在一个池子里,好处是指定了名字在稍后配置HtmlWebpackPlugin时好操作一下,缺点是如果一个chunk只依赖于池子中的某一小部分,那么也需要引用整个池子。webpack官网推荐不要配置name:’vendor’,但是在多页面的场景下,配置了name:’vendor’会更方便一下。

3.将所有动态依赖的第三方库放到各自单独的文件中,这是webpack的默认行为。

4.所有动态依赖的自研发模块分别分割到单独的文件,这也是webpack的默认行为。

5.将所有依赖的自研发模块放到同一个文件中,这里我们配置只有被依赖2次或者以上的模块才被放到common中,并且优先级低于vendor,配置如下:

1
2
3
4
5
6
7
8
9
...
common: {
chunks: "all",
minChunks: 2,
name:'common',
enforce: true,
priority: 5
},
...

yarn build打包结果多了一个common.6d1f3c64bb3f7cea425f.js文件,其中包含了A-module,F-module,G-moduel,H-module和D-module;而对于E-module,由于只被C-module依赖了一次,因此直接消化在了C-module对应的输出文件中。

6.配置缓存需要用到HashedModuleIdsPlugin

1
2
3
4
5
...
const webpack = require('webpack');
...
new webpack.HashedModuleIdsPlugin()
...

看下此时输出的3个index.html文件引入的js:

index1.html:

1
2
3
4
5
6
<script type="text/javascript" src="manifest.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="vendor.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="common.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index1.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index2.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index3.6d1f3c64bb3f7cea425f.js"></script>

index2.html:

1
2
3
4
5
6
<script type="text/javascript" src="manifest.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="vendor.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="common.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index1.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index2.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index3.6d1f3c64bb3f7cea425f.js"></script>

index3.html:

1
2
3
4
5
6
<script type="text/javascript" src="manifest.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="vendor.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="common.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index1.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index2.6d1f3c64bb3f7cea425f.js"></script>
<script type="text/javascript" src="index3.6d1f3c64bb3f7cea425f.js"></script>

可以看到所有index.html文件中都包含了所有的依赖文件,这当然不是我们想要的结果,因此修改HtmlWebpackPlugin插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new HtmlWebpackPlugin({
template: './src/index1.html',
filename: 'index1.html',
chunks:['index1','manifest','vendor','common']
}),
new HtmlWebpackPlugin({
template: './src/index2.html',
filename: 'index2.html',
chunks:['index2','manifest','vendor','common']

}),
new HtmlWebpackPlugin({
template: './src/index3.html',
filename: 'index3.html',
chunks:['index3','manifest','vendor','common']

}),

HtmlWebpackPlugin依赖于chunk的名字,这也是为什么前文提到需要为cacheGroup设置name字段的原因,不然webpack会自动为我们生成很多动态的名字,这样无法配置HtmlWebpackPlugin。

多入口项目建议配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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;

module.exports = {
entry: {
'index1': './src/index1.js',
'index2': './src/index2.js',
'index3': './src/index3.js'
},
output: {
filename: '[name].[hash].js',
chunkFilename: '[name].[hash].js',
path: path.resolve(__dirname, 'dist')
},
//use inline-source-map for development:
devtool: 'inline-source-map',

//use source-map for production:
// devtool: 'source-map',
devServer: {
contentBase: './dist'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
include: [path.resolve(__dirname, 'src')],
exclude: [path.resolve(__dirname, 'node_modules')]
}
]
},
plugins: [
new CleanWebpackPlugin(),
new BundleAnalyzerPlugin({
openAnalyzer: true,
analyzerMode: 'static',
reportFilename: 'bundle-analyzer-report.html'
}),
new HtmlWebpackPlugin({
template: './src/index1.html',
filename: 'index1.html',
chunks: ['index1','manifest','vendor','common']
}),
new HtmlWebpackPlugin({
template: './src/index2.html',
filename: 'index2.html',
chunks: ['index2','manifest','vendor','common']
}),
new HtmlWebpackPlugin({
template: './src/index3.html',
filename: 'index3.html',
chunks: ['index3','manifest','vendor','common']
}),
// 解决 hash 频繁变动的问题,设置缓存
new webpack.HashedModuleIdsPlugin()
],
optimization: {
runtimeChunk: {
"name": "manifest"
},
splitChunks: {
cacheGroups: {
default: false,
vendors: false,
vendor: {
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
enforce: true,
priority: 10,
name:'vendor'
},
common: {
chunks: "all",
minChunks: 2,
name:'common',
enforce: true,
priority: 5
},
}
}
}
}

本文标题:webpack4笔记(3)-代码分割

文章作者:tongtong

发布时间:2019年04月21日 - 23:04

最后更新:2019年04月23日 - 19:04

原始链接:https://ilove-coding.github.io/2019/04/21/webpack4笔记(3)-代码分割/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------本文结束-------------