什么是分包?
最先接触到“分包”的概念是在开发微信小程序时:
某些情况下,开发者需要将小程序划分成不同的子包,在构建时打包成不同的分包,用户在使用时按需进行加载。
...
对小程序进行分包,可以优化小程序首次启动的下载时间,以及在多团队共同开发时可以更好的解耦协作。
而微信小程序的分包也很简单,根据页面来配置即可。
不过,在了解Webpack的编译过程后,对分包的概念会有更清晰的理解: 如果两个chunk引入了同样的公共模块(例如lodash),那么在这两个chunk打包后生成的文件里面都包含这些代码,这样就大量增加了最终打包结果的重复代码,这样就增加了文件的体积,不利于传输。另外,如果把这种大量的公共代码单独打包成一个文件,就可以充分利用浏览器的缓存,提高了资源利用率。
Webpack的分包机制主要是通过代码分割(code splitting)来实现,简单来说,就是将一个完整的代码,分布到不同的打包文件中。
分包的目的是通过降低总体积和利用浏览器缓存,来优化Web应用的加载性能和资源利用率。
什么时候要分包?
通过上面结合webpack编译的分析,我们可以知道分包主要是解决这个情况的:多个chunk引入同样的公共模块,并且公共模块的代码量很大。
所以使用场景如下
- 大型单页面应用(SPA):单页面应用有多个路由或页面,其中只有部分页面或功能区域实际使用了大型库如 lodash。将 lodash 或其他大型库分割成一个单独的 chunk。在进入每个页面时不需要重新加载整个库。另外如果使用了大型框架或库(如React、Angular、Vue等),单一的打包文件可能会非常庞大,导致加载时间过长。
- 有多个入口点:例如不同的页面或路由,分包可以为每个入口点生成单独的包。
- 模块复用和共享:通过分包,可以更好地组织和管理应用的模块,提高代码的复用性和可维护性。
- 按需加载资源:一些资源可能只有在特定条件下才需要加载,例如某些功能或特定用户权限下的代码。分包可以根据需要动态加载这些资源,而不是在初始加载时全部加载,从而节省带宽和提升加载速度。
- 不同环境使用不同的分包策略:例如开发环境可能不需要分包,而生产环境需要。
- ......
分包方式
Webpack中的自动分包和手动分包是两种不同的代码拆分策略,它们的主要区别在于如何定义和触发代码块的分割。
- 手动分包是通过开发者明确地指定哪些模块或代码应该被分割成独立的代码块,提供了更精确的控制;
- 自动分包是Webpack根据其内部算法和配置自动决定如何分割代码块,而无需开发者显式地指定,使用更简便;
注意:
- 手动分包和自动分包原理不同,可以根据具体场景来决定使用哪种方式!
- 分包可以在开发环境和生产环境中使用,但大多数情况都是在生产环境使用。这里的手动分包和自动分包都是在生产环境使用。
手动分包
思路:先单独打包公共模块,再根据入口模块正常打包
单独打包公共模块示意:
公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单(manifeat.json)
打包时参考资源清单文件,发现资源清单里已经有这个模块的描述了,此时不需要再把这个文件合并到打包的文件中了。这样,就能实现了打包后的减少重复代码。
具体操作
这里还是以lodash和jquery示例
步骤一: 单独打包公共模块
这是一个独立的打包过程,需要单独建一个配置文件:webpack.dll.config.js
const webpack = require("webpack");
const path = require("path");
module.exports = {
mode: "production",
entry: {
jquery: ["jquery"], // 这里需要注意写成数组形式
lodash: ["lodash"]
},
output: {
filename: "dll/[name].js", // chunk打包后放到dist/dll目录
library: "[name]" ,// 每个bundle暴露的全局变量名
//libraryTarget: "" 暴露方式,默认为var 可选umd
},
// 利用DllPlugin生成资源清单
plugins: [
new webpack.DllPlugin({
//资源清单的保存位置,不在dist目录下
path: path.resolve(__dirname, "dll", "[name].manifest.json"),
name: "[name]"//资源清单中,暴露的变量名
})
]
};
dll清单不需要在最终运行时使用,是在后续打包外面自己的代码的过程中运行的,所以没必要放在dist目录下。
在webpack.config.js
中配置打包公共模块的命令,指定该配置文件。
{
"scripts" : {
"dll": "webpack -- config webpack.dll.config.js"
}
}
dll清单打包结果:lodash.manifest.json
此时的目录结构:
|—— project
|—— dist
|—— dll
|—— jquery.js
|—— lodash.js
|—— index.html
|—— main.96075.js
|—— ...
|—— dll
|—— jquery.manifest.json
|—— lodash.manifest.json
|——src
|—— index.js
|—— ...
|——webpack.config.js
步骤二:根据入口模块正常打包
1. 在页面中手动引入公共模块
public/index.html
(生成页面的template):
// 页面打包后在dist目录下,所以./dll/jquery.js引入公共模块
// 引入后可以暴露全局变量jquery,lodash
<script src="./dll/jquery.js"></script>
<script src="./dll/lodash.js"></script>
2. 避免打包好的公共模块被删除
clean-webpack-plugin
会清空dist目录,这样刚才打包的dll-jquery.js和dll-lodash.js就没了😱
所以如果使用了clean-webpack-plugin
,需要重新设置,使其忽略掉刚才打包好的公共模块
// webpack.config.js
new CleanWebpackPlugin({
// 要清除的文件或目录
// 排除掉dll目录本身和它里面的文件
cleanOnceBeforeBuildPatterns: ["**/*", '!dll', '!dll/*']
})
3. 通过DllReferencePlugin
使用资源清单
// webpack.config.js
module.exports = {
//...
plugins:[
//...
new webpack.DllReferencePlugin({
manifest: require("./dll/jquery.manifest.json")
}),
new webpack.DllReferencePlugin({
manifest: require("./dll/lodash.manifest.json")
})
]
}
最后进行正常打包
总结
注意:不要对小型的公共js库使用。因为代码不多,即使有重复,代码也不会增加很多。如果使用分包反而增加了文件的数量,从而增加请求的数量
优点:
- 极大提升自身模块的打包速度
- 极大的缩小了自身文件体积
- 有利于浏览器缓存第三方库的公共代码
缺点:
- 使用非常繁琐
- 如果第三方库中包含重复代码,则效果不太理想
自动分包
手动分包可以极大地提高模块的构建速度,自动分包的构建性能会降低。但自动分包提升了开发效率,有新模块引入时不需要手动处理
官方文档:webpack.js.org/configurati…
原理
不同与手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体哪个包要分出去进行控制
要控制自动分包,关键是要配置一个合理的分包策略
有了分包策略之后,不需要额外安装任何插件,webpack会自动按照策略进行分包
自动分包主要是使用SplitChunksPlugin
的功能!
官方文档SplitChunksPlugin | webpack
过程示意图:
配置分包策略
webpack的配置项Optimization | webpack
其中,分包策略配置项为Optimization | webpack
// webpack.config.js
module.exports = {
// ...
optimization: {
splitChunks: {
// 分包策略
}
}
}
1. 配置项-chunks
该配置项用于配置需要应用分包策略的chunkSplitChunksPlugin | webpack
chunks的取值:
- all: 对于所有的chunk都要应用分包策略
- async:【默认】仅针对异步chunk应用分包策略(类似于懒加载)
- initial:仅针对普通chunk应用分包策略
- 使用的是 webpack 版本 5.86.0 及以上版本,可以传递正则表达式
想要测试自动分包的效果,可以把chunks改为all
2. 配置项-maxSize
该配置可以控制包的最大字节数
如果某个包(包括分出来的包)超过了该值,则webpack会尽可能的将其分离成多个包。
但是,分包的基础单位是模块,如果一个完整的模块超过了该体积,它是无法做到再切割的,因此,尽管使用了这个配置,完全有可能某个包还是会超过这个体积。
另外,该配置的实际意义其实不大。
分包的目的是`提取大量的公共代码,从而减少总体积和充分利用浏览器缓存`。
虽然该配置可以把一些包进行再切分,但是实际的总体积和传输量并没有发生变化。
有的浏览器支持多线程同步下载,这样的话拆分还是有一点点意义
如果要进一步减少公共模块的体积,只能是`压缩`和`tree shaking`
3. 配置项-minChunks
一个模块被多少个chunk使用时,才会进行分包,默认值1
可以尝试改为2
4. 配置项-minSize
当分包达到多少字节后才允许被真正的拆分,默认值30000
...
缓存组
前面配置的分包策略是全局的,而实际上,分包策略是基于缓存组的。
splitchunkscachegroups | webpack
每个缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不需要再次分包
默认情况下,webpack提供了两个缓存组:
module.exports = {
// ...
optimization:{
splitChunks: {
//全局配置
cacheGroups: {
// 属性名是缓存组名称,会影响到分包的chunk名
// 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己特殊的配置
vendors: {
test: /[\\/]node_modules[\\/]/, // 当匹配到相应模块时,将这些模块进行单独打包
priority: -10 // 缓存组优先级,优先级越高,该策略越先进行处理,默认值为0
},
default: {
minChunks: 2, // 覆盖全局配置,将最小chunk引用数改为2
priority: -20, // 优先级
reuseExistingChunk: true // 重用已经被分离出去的chunk
}
}
}
}
}
很多时候,缓存组对于我们来说没什么意义,因为默认的缓存组就已经够用了
但是我们同样可以利用缓存组来完成一些事情,比如对公共样式的抽离:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
styles: {
test: /\.css$/, // 匹配样式模块
minSize: 0, // 覆盖默认的最小尺寸,这里仅仅是作为测试
minChunks: 2 // 覆盖默认的最小chunk引用数
}
}
}
},
module: {
rules: [{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, "css-loader"] }]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: "./public/index.html",
chunks: ["index"]
}),
new MiniCssExtractPlugin({
filename: "[name].[hash:5].css",
// chunkFilename是配置来自于分割chunk的文件名
chunkFilename: "common.[hash:5].css"
})
]
}
注:此处可以看到也多了一个styles的js文件,这是由于commonJS开启了css module,需要导出一个对象,这里的js是为了满足css module
总结
自动分包的原理其实并不复杂,主要经过以下步骤:
-
检查每个chunk编译的结果
-
根据分包策略,找到那些满足策略的模块
-
根据分包策略,生成新的chunk打包这些模块(代码有所变化)
-
把打包出去的模块从原始包中移除,并修正原始包代码
画个图演示:
从上图可以看到,在代码层面,有以下变动
- 分包的代码中,加入一个全局变量,类型为数组,其中包含公共模块的代码
2. 原始包的代码中,使用数组中的公共代码
———————————————————End—————————————————————
🌞注:以上手动分包和自动分包的内容参考了渡一前端课程的笔记