在 Webpack4 之前,我们处理公共模块的方式都是使用 CommonsChunkPlugin,然后该插件让开发配置繁琐,并且 使公共代码的抽离不够彻底和细致,因此新的splitChunks改进了这些能力,不过虽然splitChunks相对 CommonsChunkPlugin 进步不少,但是 splitChunks 的配置却比较复杂。
Since version 4 the
CommonsChunkPlugin
was removed in favor ofoptimization.splitChunks
andoptimization.runtimeChunk
options. Here is how the new flow works.
Webpack 代码拆分方式
在 Webpack 中,总共提供了三种方式来实现代码拆分(Code Splitting)
- entry 配置:通过多个entry文件来实现
- 动态加载(按需加载):通过写代码时主动使用 import() 或者 require.ensure 来动态加载
- 抽取公共代码:使用 splitChunks 配置来抽取公共代码
回顾一下三个重要的概念 : module chunks bundle
- module: 就是 JavaScript的模块,简单来说就是你通过 import require语句引入的代码,也包括css,图片等资源
- chunk:chunk是webpack根据功能拆分出来的,chunk包含着module,可能是一对多也可能是一对一,chunk包含三种情况,就是上面介绍的三种实现代码拆分的情况
- bundle:bundle是webpack打包之后的各个文件,一般就是和chunk是一对一的关系,bundle就是对chunk进行编译压缩打包等处理之后的产出
splitChunks 默认配置
module.exports = {
// ...
optimization: {
splitChunks: {
chunks: "async", // 三选一: "initial" | "all" | "async" (默认)
minSize: 30000, // 最小尺寸,30K,development 下是10k,越大那么单个文件越大,chunk 数就会变少(针对于提取公共 chunk 的时候,不管再大也不会把动态加载的模块合并到初始化模块中)当这个值很大的时候就不会做公共部分的抽取了
maxSize: 0, // 文件的最大尺寸,0为不限制,优先级:maxInitialRequest/maxAsyncRequests < maxSize < minSize
minChunks: 1, // 默认1,被提取的一个模块至少需要在几个 chunk 中被引用,这个值越大,抽取出来的文件就越小
maxAsyncRequests: 5, // 在做一次按需加载的时候最多有多少个异步请求,为 1 的时候就不会抽取公共 chunk 了
maxInitialRequests: 3, // 针对一个 entry 做初始化模块分隔的时候的最大文件数,优先级高于 cacheGroup,所以为 1 的时候 就不会抽取 initial common 了 automaticNameDelimiter: '~', // 打包文件名分隔符
name: true, // 拆分出来文件的名字,默认为 true,表示自动生成文件名,如果设置为固定的字符串那么所有的 chunk 都会被合 并成一个
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/, // 正则规则,如果符合就提取 chunk
priority: -10, // 缓存组优先级,当一个模块可能属于多个 chunkGroup,这里是优先级
},
default: {
minChunks: 2,
priority: -20, // 优先级
reuseExistingChunk: true, // 如果该chunk包含的modules都已经另一个被分割的chunk中存在,那么直接引用已存在的c hunk,不会再重新产生一个
},
},
},
},
};
splitChunks默认配置对应的就是 chunk 生成的第二种情况:通过写代码时主动使用import()或者require.ensure来动态加载。
下面来看下使用import()或者require.ensure来写代码,在 Webpack 打包的时候有什么不同。
创建index.js,使用import()动态加载react模块,同时为了方便跟踪产出物,在这里使用了 同时为了方便跟踪产出物,在这里使用了 webpack 的魔 的魔 法注释,保证输出的 法注释,保证输出的 bundle 名称,后面也使用这种方式 名称,后面也使用这种方式。内容如下
import(/* webpackChunkName: "react" */ 'react');
添加webpack.config.js,内容如下:
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
module.exports = {
mode: "production",
entry: { main: "./default/index.js" },
plugins: [new BundleAnalyzerPlugin()],
};
npm install --save-dev webpack-bundle-analyzer
完成上面配置之后,执行webpack —config webpack.config.js,首先看到对应的输出的 log 为:
如果 提示 webpack not found . 尝试另一种方法:在package.json 中配置
{
"scripts": {
"dev": "webpack --config webpack.dev.js --progress"
}
}
理解 splitChunks.chunks
三个值
splitChunks中的chunks是一个很重要的配置项,表示从哪些 chunks 里面抽取代码,chunks的三个值有:"init ial"、 "all"、 "async"
, 默认就是是async
。
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
.BundleAnalyzerPlugin;
module.exports = {
mode: "development",
entry: { a: "./src/a.js", b: "./src/b.js" },
plugins: [new BundleAnalyzerPlugin()],
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
chunks: "async",
test: /[\\/]node_modules[\\/]/,
},
},
},
},
};
chunks=’async’
当 chunks=’async’配置下Webpack 打包 log和bundle分析结果如下
在这种模式下:
1.在 a.js 和 b.js 都同步引入的 jquery 被打包进了各自的 bundle 中没有拆分共用,说明在这种配置下只会针对动态引入的代码进行拆分
2.react 在 a.js 和 b.js 表现不同:
1)在a.js因为是同步引入的,设置的 chunks=’async’ ,所以不被拆分出去
2)在b.js是动态引入的,符合 chunks=’async’ 的设置,所以被单独拆到 vendors~b-react.js
3.lodash 因为在两个文件都是动态加载的,所以被拆分到了 vendors~a-lodashjs
Tips:b.js中的react拆出来的文件名是vendors~b-react.js含有vendors,说明中了名字为vendors的cacheGroups规则
chunks=’initial’
当 chunks=’initial’配置下Webpack 打包 log和bundle分析结果如下
initial 即原始的,最初的意思。原则就是有共用的情况即发生拆分。首先,动态引入的模块不受影响,它是无论如何都会被拆分出去的。而对于同步引入的代码,如果有多处都在使用,则拆分出来共用,至于共同引用多次会被拆分,是通过 minChunks 单独配置的。针对这个原则,我们再来看下上面的代码拆分的结果
1.因为 jquery 模块 是 a.js 和 b.js 共用的代码,所以单独拆分到 vendors~a~b.js中,vendors~a~b.js文件名 来自我们配置的cacheGroups的 key,即vendors和分隔符(automaticNameDelimiter)以及实际被共用的 bundle 的名称,即:a 和 b;
2.react 在 b.js 因为用的是动态引入,所以被拆分成了 b-react.js(名字来自于设置的魔法注释) a.js的react则被拆分到了vendors-a.js
3.lodash因为在两个文件都是动态加载的,所以被拆到了a-lodash.js(名字来自魔法注释)
进一步解释:react在b.js拆出来为b-react.js名称,说明中了默认配置(默认配置 是chunks=’async’),名字来自魔法注释;a.js的react文件名是vendors~a.js,这是因为中了vendors规 则,本身a.js的react是同步引入,在这里被拆出来是因为react在 development 模式用的是 dev 版本,体 积超过minSize的默认设置30K,所以被拆出来了,观察对应的 webpack 打包 log:Entrypoint a = vendor s~a~b.js vendors~a.js a.js 也说明这一点。如果我们把对应的配置,加大minSize到 80K(超过 dev 版本 react 大小),则vendors~a.js 和 a.js会合并在一起了,具体看下图效果:
// 忽略其他,只看cacheGroups
vendors: {
minSize: 80000,
chunks: 'initial',
test: /[\\/]node_modules[\\/]/
}
chunks=’all’
当 chunks=’initial’配置下,虽然a.js 和 b.js 都引入了react,但是因为引入方式不同,而没有拆分在一起,而是各自单独拆分成一个chunk,要想把react放到一个文件中,就要使用 chunks=’all’了。下面是配置结果
通过执行打包结果,跟我们的预期一致,chunks='all'
的配置下能够最大程度的生成复用代码,复用代码在 httpcache 环境下,多页应用由一个页面跳转到另外一个共用代码的页面,会节省 http 请求,所以一般来说chunks='all'
是推荐的方式,但是async
和initial
也有其存在的必要,理解三者差异,根据项目实际代码拆分需求来配置即可。
Tips:拆分出来的文件名称可以通过output.chunkFilename来指定规则,例 如chunkFilename=’[name].js’,然后在对应的配置中配置name的具体值,比如 vendors 的 name 指定为foo:vendors.name=’foo’
使用 cacheGroups
cacheGroups(缓存组)是 Webpack splitChunks 最核心的配置,splitChunks的配置项都是作用于cacheGroup上 的,默认有两个cacheGroup:vendors和default(本文一开始默认配置部分已经贴出),如果将cacheGroup的 默认两个分组vendor和default设置为 false,则splitChunks就不会起作用,我们也可以重写这俩默认的配置。
cacheGroups除了拥有默认配置所有的配置项目(例如 minSize、minChunks、name 等)之外,还有三个独有的配 置项:test、priority和reuseExistingChunk。 splitChunks.cacheGroup必须同时满足 必须同时满足各个配置项的条件才能生效
reuseExistingChunk表示是否使用已有的 chunk,如果为 true 则表示如果当前的 chunk 包含的模块已经被抽取出 去了,那么将不会重新生成新的。下面重点说下test和priority
Tips:除了 JavaScript,splitChunks也适用于使用mini-css-extract-plugin插件的 css 配置。
priority
priority配置项的意义就是权重。如果有一个模块满足了多个缓存组的条件就会去按照权重划分,谁的权重高就
优先按照谁的规则处理。
test
cacheGroup.test表示满足这个条件的才会被缓存组命中,取值可以是正则、字符串和函数。正则和字符串很好理 解,当test为函数时,比如返回true/false,并且接收两个参数:module和chunks
- module:每个模块打包的时候,都会执行test函数,并且传入模块 module 对象,module 对象包含了模块的基 本信息,例如类型、路径、文件 hash 等;
- chunks:是当前模块被分到哪些chunks使用,module 跟 chunks 关系可能是一对一,也可能是多对一,所以一 旦我们使用 chunks 做匹配,那么符合条件的 chunk 内包含的模块都会被匹配到
module.exports = {
//...
optimization: {
splitChunks: {
cacheGroups: {
vendors: {
test(module, chunks) {
//...
return module.type === "javascript/auto";
},
},
},
},
},
};
举个实际应用场景来说明,前面提到过splitChunks不仅作用于 JavaScript,还可以作用于 CSS,所以类似test=/ [\/]node_modules[\/]/的写法,实际也会匹配出node_modules中的 CSS,如果我们用到的一个 npm 包引入了自己的 css 文件,那么也会中了拆分逻辑,这时候如果要排除这部分 CSS 或者单独给这部分 CSS 设置自己的cacheGroup规则,有两种方式:
- 设置更高权重的cacheGroup;
2. 使用test函数针对类型为 js 和 css 分别设置各自的cacheGroup
另外我们还可以使用test函数实现更细化的匹配,例如:忽略一部分文件等。