# 第6章 代码分片
code splitting 可以把代码按照特定的形式进行拆分,使用户不必一次全部加载,而是按需加载。
代码分片可以有效降低首屏加载资源的大小。
# CommonsChunkPlugin
CommonsChunkPlugin是webpack4之前内部自带的插件(4之后替换为了SplitChunks)。它可以将多个Chunk中公共的部分提取出来。
- 开发过程中减少了重复模块打包,可以提升开发速度。
- 减小整体资源体积。
- 合理分片后的代码可以更有效的利用客户端缓存。
const webpack = require('webpack');
module.exports = {
entry: {
foo: './foo.js',
bar: './bar.js',
},
output: {
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons', // 公共chunk的名字
filename: 'commons.js' // 资源的文件名
})
]
}
// 打包后有三个文件
// foo.js bar.js commons.js
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 提取vendor
单入口也可以使用CommonsChunkPlugin。可以用它来提取第三方类库及业务中不常更新的模块,只需要单独为它们创建一个入口。
const webpack = require('webpack');
module.exports = {
entry: {
app: './app.js',
vendor: ['react'],
},
output: {
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', // 公共chunk的名字
filename: 'vendor.js' // 资源的文件名
})
n
]
}
// 打包后
// app.js vendor.js
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在插件内部配置中,将name指定为vendor,这样由CommonsCHunkPlugin所产生的资源将覆盖原有的由vendor这个入口所产生的资源。
# 设置提取范围
通过CommonsChunkPlugin中的chunks配置项可以规定从哪些入口中提取公共模块。
const webpack = require('webpack');
module.exports = {
entry: {
a: './a.js',
b: './b.js',
c: './c.js',
},
output: {
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons', // 公共chunk的名字
filename: 'commons.js', // 资源的文件名
chunks: ['a', 'b']
})
]
}
// 打包后
// a.js b.js c.js commons.js
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Asset | Size | Chunks | Chunk Names |
---|---|---|---|
b.js | 503 bytes | 0 | b |
a.js | 503 bytes | 1 | a |
c.js | 72.1 kb | 2, 3 | c |
commons.js | 73 kb | 3 | commons |
# 设置提取规则
有时候我们不希望所有的公共模块都被提取出来,虽然被多次引用,但是可能经常修改,如果将其和react这种库放在一起反而不利于客户端缓存。
此时可以通过minChunks配置项来设置提取规则。
# 数字
minChunks可以接受一个数字,当设置minChunks为n时,只有该模块被n个入口同时引用时才会进行提取,这个值不会影响通过数组形式入口传入模块的提取
const webpack = require('webpack');
module.exports = {
entry: {
foo: './foo.js',
bar: './bar.js',
vendor: ['react']
},
output: {
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', // 公共chunk的名字
filename: 'vendor.js', // 资源的文件名
minChunks: 3
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// foo.js
import React from 'react';
import './util';
// bar.js
import React from 'react';
import './util';
// util.js
console.log('util');
2
3
4
5
6
7
8
9
10
由于minChunks为3,utils.js并不会被提取到vendor.js中,然而react并不受这个的影响,仍然会出现在vendor.js中。这就是数组形式入口的模块会照常提取。
# Infinity
设置为无穷代表提取的值无线高,也就是说所有模块都不会被提取。
- 我们只想让webpack提取特定的几个模块,并将这些模块通过数组型入口传入。
- 为了生成一个没有任何模块而仅仅包含webpack初始化环境的文件,这个文件我们称为manifest。
# 函数
webpack打包过程中的每个模块都会经过这个函数的处理,当函数的返回值为true时进行提取。
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.js',
minChunks: function(module, count) {
// module.context 模块目录路径
if(module.context && module.context.includes('mode_modules')) {
return true;
}
// module.resource 包含模块名的完整路径
if(module.resource && module.resource.endsWith('util.js')) {
return true;
}
// count 为模块被引用的次数
if(count > 5) {
return true;
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# hash与长效缓存
CommonsChunkPlugin提取公共模块时,提取后的资源内部不仅仅是模块的代码,往往还包含webpack的运行时。指的是初始化环境的代码,如创建模块缓存对象、声明模块加载函数等。
早期webpack中,运行时内部也包含模块id,并且这个id是以数字不断累加的。这会导致一个问题,模块id的改变会导致运行时内部的代码发生变化,进一步影响chunk hash的生成。一般会使用chunk hash作为资源的版本号优化客户端缓存,版本号改变会导致用户频繁的更新资源,即使它们的内容并没有发生变化也会更新。。
所以可以将运行时代码单独提取出来。
const webpack = require('webpack');
module.exports = {
entry: {
app: './app.js',
vendor: ['react']
},
output: {
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor', // 公共chunk的名字
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest', // 公共chunk的名字
})
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Asset | Size | Chunks | Chunk Names |
---|---|---|---|
vendor.js | 69.3 kb | 0 | vendor |
app.js | 506 bytes | 1 | app |
manifest.js | 3.84 kb | 2 | manifest |
manifest的CommonsChunkPlugin必须出现在最后,否则Webpack将无法正常提取模块。
我们页面中manifest.js应该最先被引入,用来初始化webpack环境。通过这种方式,app.js中的变化将只会影响manifest.js,而它是一个很小的文件,vendor.js内容及hash都不会变化,因此可以被用户所缓存。
# CommonsChunkPlugin的不足
- 一个CommonsChunkPlugin只能提取一个vendor,如果想提取多个vendor则需要配置多个插件,会增多重复的配置代码。
- manifest实际上会使浏览器多加载一个资源,对于页面渲染速度不是友好的。
- 由于内部设计缺陷,在提取公共模块的时候会破坏掉原有Chunk中模块的依赖关系,导致难以进行更多的优化。比如在异步Chunk的场景下CommonsChunkPlugin并不会按照预期正常工作。
// webpack.config.js
const webpack = require('webpack');
module.exports = {
entry: {
foo: './foo.js'
},
output: {
filename: '[name].js'
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'commons', // 公共chunk的名字
filename: 'commons.js'
})
]
}
// foo.js
import React from 'react';
import ('./bar.js');
// bar.js
import React from 'react';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
结果如下:
Asset | Size | Chunks | Chunk Names |
---|---|---|---|
0.foo.js | 503 bytes | 0 | |
foo.js | 69.8 kb | 1 | main |
commons.js | 5.78 kb | 2 | commons |
# optimization.SplitChunks
它是webpack4为了改进CommonsChunkPlugin重新设计和实现的代码分片特性。
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
// foo.js
import React from 'react';
import ('./bar.js');
// bar.js
import React from 'react';
2
3
4
5
6
7
8
9
10
11
12
13
14
15
chunks: 'all'
含义是SplitChunks将会对所有的chunks生效(默认情况下,SplitChunks只对异步chunks生效,并且不需要配置)
Asset | Size | Chunks | Chunk Names |
---|---|---|---|
foo.js | 8.95 kb | main | main |
0.foo.js | 723 bytes | 0 | |
vendors~main.foo.js | 61.1 kb | vendors~main | vendors~main |
0.foo.js是异步加载bar.js的结果,vendors~main.foo.js是foo.js和0.foo.js提取出的react的公共模块。
# splitChunks默认的提取条件
- 提取后的chunk可被共享或者来自node_modules目录。
- 提取后的js chunk体积大于30kb, css chunk体积大于50kb。
- 在按需加载过程中,并行请求的资源最大值小于等于5。按需加载是通过动态插入script标签的方式加载脚本。因为每一个请求都要花费建立连接和释放连接的成本,因此提取规则只在请求不多的时候生效。
- 首次加载时,并行请求的资源数最大值小于等于3。
示例:
- react是node_modules模块。
- react体积大于30kb。
- 按需加载时并行请求数为1,0.foo.js。
- 首次加载时并行请求数为2,foo.js和vendors~main.foo.js
# 默认的异步提取
SplitChunks不需要配置也能生效,但仅仅针对异步资源。
module.exports = {
entry: './foo.js',
output: {
filename: 'foo.js'
}
}
// foo.js
import ('./bar.js');
// bar.js
import React from 'react';
2
3
4
5
6
7
8
9
10
11
12
13
Asset | Size | Chunks | Chunk Names |
---|---|---|---|
foo.js | 8.95 kb | main | main |
0.foo.js | 723 bytes | 0 | |
1.foo.js | 61.1 kb | 1 |
按需加载时并行请求数为2,0.foo.js和1.foo.js .
首次加载时并行请求数为1,foo.js。1.foo.js是异步加载的,所以不算。
# 配置
splitChunks默认配置:
splitChunks: {
chunks: 'async', // initial 只对入口chunk生效
minSize: {
javascript: 30000,
style: 50000
},
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# import()
Webpack中有两种异步加载的方式:import函数和require.ensure。
通过import函数加载的模块及其依赖会被异步的进行加载,并返回一个Promise对象。
// foo.js
import('./bar.js').then(({ add }) => {
console.log(add(2, 3))
})
// basr.js
export function add(a, b) {
return a + b;
}
2
3
4
5
6
7
8
9
# 异步chunk的配置
output: {
filename: '[name].js',
chunkFilename: '[name].js'
}
// foo.js
import(/* webpackChunkName: "bar" */ './bar.js').then(({add}) => {
console.log(add(2, 3));
})
2
3
4
5
6
7
8
9
← 第5章 样式处理 第7章 生产环境配置 →