共计 6566 个字符,预计需要花费 17 分钟才能阅读完成。
前言
开发多页应用的时候,如果不对 webpack 打包进行优化,当某个模块被多个入口模块引用时,它就会被打包多次(在最终打包出来的某几个文件里,它们都会有一份相同的代码)。当项目业务越来越复杂,打包出来的代码会非常冗余,文件体积会非常庞大。大体积文件会增加编译时间,影响开发效率;如果直接上线,还会拉长请求和加载时长,影响网站体验。作为一个追求极致体验的攻城狮,是不能忍的。所以在多页应用中优化打包尤为必要。那么如何优化 webpack 打包呢?
一、概念
在一切开始前,有必要先理清一下这三个概念:
- module: 模块,在 webpack 眼里,任何可以被导入导出的文件都是一个模块。
-
chunk: chunk 是 webpack 拆分出来的:
- 每个入口文件都是一个 chunk
- 通过 import、require 引入的代码也是
- 通过 splitChunks 拆分出来的代码也是
- bundle: webpack 打包出来的文件,也可以理解为就是对 chunk 编译压缩打包等处理后的产出。
二、问题分析
首先,简单分析下,我们刚才提到的打包问题:
- 核心问题就是:多页应用打包后代码冗余,文件体积大。
- 究其原因就是:相同模块在不同入口之间没有得到复用,bundle 之间比较独立。
弄明白了问题的原因,那么大致的解决思路也就出来了:
- 我们在打包的时候,应该把不同入口之间,共同引用的模块,抽离出来,放到一个公共模块中。这样不管这个模块被多少个入口引用,都只会在最终打包结果中出现一次。——解决代码冗余。
- 另外,当我们把这些共同引用的模块都堆在一个模块中,这个文件可能异常巨大,也是不利于网络请求和页面加载的。所以我们需要把这个公共模块再按照一定规则进一步拆分成几个模块文件。——减小文件体积。
-
至于如何拆分,方式因人而异,因项目而异。我个人的拆分原则是:
- 业务代码和第三方库分离打包,实现代码分割;
- 业务代码中的公共业务模块提取打包到一个模块;
- 第三方库最好也不要全部打包到一个文件中,因为第三方库加起来通常会很大,我会把一些特别大的库分别独立打包,剩下的加起来如果还很大,就把它按照一定大小切割成若干模块。
optimization.splitChunks
webpack 提供了一个非常好的内置插件帮我们实现这一需求:CommonsChunkPlugin
。不过在 webpack4 中 CommonsChunkPlugin
被删除,取而代之的是 optimization.splitChunks
, 所幸的是optimization.splitChunks
更强大!
三、实现
通过一个多页应用的小 demo,我们一步一步来实现上述思路的配置。
demo 目录结构:
|--public/
| |--a.html
| |--index.html
|--src/
| |--a.js
| |--b.js
| |--c.js
| |--index.js
|--package.json
|--webpack.config.js
代码逻辑很简单,index
模块中引用了 a
和 b
2 个模块,a
模块中引用了 c
模块和 jquery
库,b
模块中也引用了 c
模块和 jquery
库,c
是一个独立的模块没有其他依赖。
index.js 代码如下:
//index.js
import a from './a.js';
import b from './b.js';
function fn() {console.log('index-------');
}
fn();
a.js 代码如下:
//a.js
require('./c.js');
const $ = require('jquery')
function fn() {console.log('a-------');
}
module.exports = fn();
b.js 代码如下:
//b.js
require('./c.js');
const $ = require('jquery')
function fn() {console.log('b-------');
}
module.exports = fn();
c.js 代码如下:
//c.js
function fn() {console.log('c-------');
}
module.exports = fn();
1. 基本配置
webpack 先不做优化,只做基本配置,看看效果。项目配置了 2 个入口,搭配 html-webpack-plugin
实现多页打包:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
a: './src/a.js'
},
output: {path: path.resolve(__dirname, 'dist'),
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html'
}),
new HtmlWebpackPlugin({
template: './public/a.html',
filename: 'a.html'
})
]
}
在开发模式下运行 webpack:
可以看到,打包出两个 html 和两个体积很大的(300 多 K)的文件a.js
,index.js
。
进入 dist 目录检查 js 文件:
-
a.js
里包含c
模块代码和jquery
代码 -
index.js
里包含a
模块、b
模块、c
模块和jquery
代码
看,同样的代码 c
和jquery
被打包了 2 遍。
2. 初步添加 splitChunks 优化配置
首先解决相同代码打包 2 次的问题,我们需要让 webpack 把 c
和jquery
提取出来打包为公共模块。
在 webpack 配置文件添加 splitChunks:
//webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
default: {
name: 'common',
chunks: 'initial'
}
}
}
}
– cacheGroups
-
cacheGroups
是splitChunks
配置的核心,对代码的拆分规则全在cacheGroups
缓存组里配置。 - 缓存组的每一个属性都是一个配置规则,我这里给他的
default
属性进行了配置,属性名可以不叫 default 可以自己定。 - 属性的值是一个对象,里面放的我们对一个代码拆分规则的描述。
– name
- name:提取出来的公共模块将会以这个来命名,可以不配置,如果不配置,就会生成默认的文件名,大致格式是
index~a.js
这样的。
– chunks
- chunks:指定哪些类型的 chunk 参与拆分,值可以是 string 可以是函数。如果是 string,可以是这个三个值之一:
all
,async
,initial
,all
代表所有模块,async
代表只管异步加载的,initial
代表初始化时就能获取的模块。如果是函数,则可以根据 chunk 参数的 name 等属性进行更细致的筛选。
再次打包:
可以看到 a.js
,index.js
从 300 多 K 减少到 6 点几 K。同时增加了一个 common.js
文件,并且两个打包入口都自动添加了 common.js
这个公共模块:
进入 dist 目录,依次查看这 3 个 js 文件:
-
a.js
里不包含任何模块的代码了,只有 webpack 生成的默认代码。 -
index.js
里同样不包含任何模块的代码了,只有 webpack 生成的默认代码。 -
common.js
里有a
,b
,c
,index
,jquery
代码。
发现,提是提取了,但是似乎跟我们预料的不太一样,所有的模块都跑到 common.js
里去了。
这是因为我们没有告诉 webpack(splitChunks
)什么样的代码为公共代码,splitChunks
默认任何模块都会被提取。
– minChunks
splitChunks
是自带默认配置的,而缓存组默认会继承这些配置,其中有个 minChunks
属性:
- 它控制的是每个模块什么时候被抽离出去:当模块被不同 entry 引用的次数大于等于这个配置值时,才会被抽离出去。
- 它的默认值是 1。也就是任何模块都会被抽离出去(入口模块其实也会被 webpack 引入一次)。
我们上面没有配置 minChunks
,只配置了name
和chunk
两个属性,所以 minChunks
的默认值 1
生效。也难怪所有的模块都被抽离到 common.js
中了。
优化一下,在缓存组里配置 minChunks
覆盖默认值:
//webpack.config.js
optimization: {
splitChunks: {
cacheGroups: {
default: {
name: 'common',
chunks: 'initial',
minChunks: 2 // 模块被引用 2 次以上的才抽离
}
}
}
}
然后运行 webpack
可以看到有 2 个文件的大小发生了变化:common.js
由 314K 减小到 311K,index.js
由 6.22K 增大到 7.56K。
进入 dist 目录查看:
-
a.js
里依然不包含任何模块的代码(正常,因为a
作为模块被index
引入了一次,又作为入口被 webpack 引入了一次,所以a
是有 2 次引用的)。 -
index.js
里出现了b
和index
模块的代码了。 -
common.js
里只剩a
,c
, 和jquery
模块的代码。
现在我们把共同引用的模块 a
, c
, jquery
,从a
和index
这两个入口模块里抽取到 common.js
里了。有点符合我们的预期了。
3. 配置多个拆分规则
3.1 实现代码分离,拆分第三方库
接下来,我希望公共模块 common.js
中,业务代码和第三方模块 jquery 能够剥离开来。
我们需要再添加一个拆分规则。
//webpack.config.js
optimization: {
splitChunks: {
minSize: 30, // 提取出的 chunk 的最小大小
cacheGroups: {
default: {
name: 'common',
chunks: 'initial',
minChunks: 2, // 模块被引用 2 次以上的才抽离
priority: -20
},
vendors: { // 拆分第三方库(通过 npm|yarn 安装的库)test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
priority: -10
}
}
}
}
我给 cacheGroups 添加了一个 vendors 属性(属性名可以自己取,只要不跟缓存组下其他定义过的属性同名就行,否则后面的拆分规则会把前面的配置覆盖掉)。
– minSize
minSize 设置的是生成文件的最小大小,单位是字节。如果一个模块符合之前所说的拆分规则,但是如果提取出来最后生成文件大小比 minSize 要小,那它仍然不会被提取出来。这个属性可以在每个缓存组属性中设置,也可以在 splitChunks 属性中设置,这样在每个缓存组都会继承这个配置。这里由于我的 demo 中文件非常小,为了演示效果,我把 minSize 设置为 30 字节,好让公共模块可以被提取出来,正常项目中不用设这么小。
– priority
priority 属性的值为数字,可以为负数。作用是当缓存组中设置有多个拆分规则,而某个模块同时符合好几个规则的时候,则需要通过优先级属性 priority 来决定使用哪个拆分规则。优先级高者执行。我这里给业务代码组设置的优先级为 -20,给第三方库组设置的优先级为 -10,这样当一个第三方库被引用超过 2 次的时候,就不会打包到业务模块里了。
– test
test 属性用于进一步控制缓存组选择的模块,与 chunks 属性的作用有一点像,但是维度不一样。test 的值可以是一个正则表达式,也可以是一个函数。它可以匹配模块的绝对资源路径或 chunk 名称,匹配 chunk 名称时,将选择 chunk 中的所有模块。我这里用了一个正则 /[\\/]node_modules[\\/]/
来匹配第三方模块的绝对路径,因为通过 npm 或者 yarn 安装的模块,都会存放在 node_modules 目录下。
运行一下 webpack:
可以看到新产生了一个叫 vendor.js
的文件(name 属性的值),同时 common.js
文件体积由原来的 311k 减少到了 861bytes!
进入 dist 目录,检查 js 文件:
-
a.js
里不包含任何模块代码。 -
common.js
只包含a
和c
模块的代码。 -
index.js
只包含b
和index
模块的代码。 -
vendor.js
只包含jquery
模块的代码。
现在,我们在上一步的基础上,成功从 common.js
里把第三方库 jquery
抽离出来放到了 vendor.js
里。
3.2 拆分指定文件
如果我们还想把项目中的某一些文件单独拎出来打包(比如工程本地开发的组件库),可以继续添加拆分规则。比如我的 src 下有个 locallib.js
文件要单独打包,假设 a.js
中引入了它。
//a.js
require('./c.js');
require('./locallib.js'); // 引入自己本地的库
const $ = require('jquery')
function fn() {console.log('a-------');
}
module.exports = fn();
可以这么配置:
//webpack.config.js
optimization: {
splitChunks: {
minSize: 30, // 提取出的 chunk 的最小大小
cacheGroups: {
default: {
name: 'common',
chunks: 'initial',
minChunks: 2, // 模块被引用 2 次以上的才抽离
priority: -20
},
vendors: { // 拆分第三方库(通过 npm|yarn 安装的库)test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'initial',
priority: -10
},
locallib: { // 拆分指定文件
test: /(src\/locallib\.js)$/,
name: 'locallib',
chunks: 'initial',
priority: -9
}
}
}
}
我在缓存组下又新增了一个拆分规则,通过 test 正则指定我就要单独打包 src/locallib.js
文件,并且把优先级设置为 -9,这样当它被多次引用时,不会进入其他拆分规则组,因为另外两个规则的优先级都比它要低。
运行 webpack 打包后:
可以看到新产生了一个 locallib.js
文件。进入 dist 目录查看:
-
a.js
里不包含任何模块代码。 -
common.js
只包含a
和c
模块的代码。 -
index.js
只包含b
和index
模块的代码。 -
vendor.js
只包含jquery
模块的代码。 -
locallib.js
里只包含locallib
模块的代码。
现在我们又在上一步的基础上独立打包了一个指定的模块locallib.js
。
至此,我们就成功实现了抽离公共模块、业务代码和第三方代码剥离、独立打包指定模块。
对比一下,优化前,打包出来 js 一共有 633KB:
优化后,打包出来 js 一共不到 330KB:
优化打包后的文件分类清晰,体积比优化前缩小了几乎 50%,有点小完美是不是!击掌!这还只是我举的一个简单例子,在实际多页应用中,优化力度说不定还不止这么多。
小结
webpack 很强大,以上只是冰山一角,但是只要掌握了上述 optimization.splitChunks
的核心配置,我们就可以几乎随心所欲地按照自己的想法来拆分优化代码控制打包文件了,是不是很酷?玩转代码拆分,你也可以!
如果觉得这些依然不能满足你的需求,还想更精 (bian) 细(tai)地定制打包规则,可以到 webpack 官网查看
optimization.splitChunks
的更多配置。
欢迎交流~
本文的完整 webpack 配置和 demo 源码可以在这里获取:
https://github.com/yc111/webp…
—
欢迎转载,转载请注明出处:
https://champyin.com/2019/11/…
本文同步发表于:
webpack 优化之玩转代码分割和公共代码提取 | 掘金