如何使用 splitChunks 对代码分割进行精细控制
背景
前端小伙伴都知道,为了降低包大小,经常会把依赖的前端模块独立打包,比如把 vue
、vue-router
打到一个单独的包 vendor
中。另外,常会将存在多个路由的复杂页面的每个页面都单独打一个包,只有访问某个页面的时候,再去下载该页面的js包,以此来加快首页的渲染。
无论是 react
还是 vue
都提供了完善的工具,帮我们屏蔽了繁琐的配置工作。当我们对代码进行构建时,已经自动帮我们完成了代码的拆分工作。
所以,很多小伙伴并不知道背后到底发生了什么事。至于为什么这么拆分,到底如何控制代码的拆分,更是一头雾水了。
问题测验
讲解开始之前,大家先看一个问题。如果你已经知道问题的答案,而且明白为什么,就不必往下阅读了。如果不知道答案或者知道答案,但不知道原因。那么,强烈建议阅读本文。
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: { app: "./src/index.js" },
output: {
filename: "[name].js",
path: path.resolve(__dirname, "dist")
},
optimization: {
splitChunks: {
chunks: "all"
}
},
plugins: [
new HtmlWebpackPlugin()
]
};
// index.js
import "vue"
import(/*webpackChunkName: 'a' */ "./a");
import(/*webpackChunkName: 'b' */ "./b");
// a.js
import "vue-router";
import "./someModule"; // 模块大小大于30kb
// b.js
import "vuex";
import "./someModule"; // 模块大小大于30kb
// someModule.js
// 该模块大小超过30kb
// ...
代码分割的三种方式
webpack 中以下三种常见的代码分割方式:
- 入口起点:使用
entry
配置手动地分离代码。 - 动态导入:通过模块的内联函数调用来分离代码。
- 防止重复:使用
splitChunks
去重和分离 chunk。
第一种方式,很简单,只需要在 entry
里配置多个入口即可:
entry: { app: "./index.js", app1: "./index1.js" }
第二种方式,就是在代码中自动将使用 import()
加载的模块分离成独立的包:
//...
import("./a");
//...
第三种方式,是使用 splitChunks
插件,配置分离规则,然后 webpack
自动将满足规则的 chunk
分离。一切都是自动完成的。
前两种拆分方式,很容易理解。本文主要针对第三种方式进行讨论。
splitChunks 代码拆分
splitChunks
默认配置
splitChunks: {
// 表示选择哪些 chunks 进行分割,可选值有:async,initial和all
chunks: "async",
// 表示新分离出的chunk必须大于等于minSize,默认为30000,约30kb。
minSize: 30000,
// 表示一个模块至少应被minChunks个chunk所包含才能分割。默认为1。
minChunks: 1,
// 表示按需加载文件时,并行请求的最大数目。默认为5。
maxAsyncRequests: 5,
// 表示加载入口文件时,并行请求的最大数目。默认为3。
maxInitialRequests: 3,
// 表示拆分出的chunk的名称连接符。默认为~。如chunk~vendors.js
automaticNameDelimiter: '~',
// 设置chunk的文件名。默认为true。当为true时,splitChunks基于chunk和cacheGroups的key自动命名。
name: true,
// cacheGroups 下可以可以配置多个组,每个组根据test设置条件,符合test条件的模块,就分配到该组。模块可以被多个组引用,但最终会根据priority来决定打包到哪个组中。默认将所有来自 node_modules目录的模块打包至vendors组,将两个以上的chunk所共享的模块打包至default组。
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
//
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
}
}
}
以上配置,可以概括为如下4个条件:
- 模块在代码中被复用或者来自
node_modules
文件夹 - 模块的体积大于等于30kb(压缩之前)
- 当按需加载 chunks 时,并行请求的最大数量不能超过5
- 页面初始加载时,并行请求的最大数量不能超过3
// index.js
import("./a");
// ...
// a.js
import "vue";
// ...
以上代码,在默认配置下的构建结果如下:
原因分析:
-
index.js
作为入口文件,属于入口起点手动配置分割代码的情况,因此会独立打包。(app.js) -
a.js
通过import()
进行加载,属于动态导入的情况,因此会独立打出一个包。(1.js) -
vue
来自node_modules
目录,并且大于30kb;将其从a.js
拆出后,与a.js
并行加载,并行加载的请求数为2,未超过默认的5;vue
拆分后,并行加载的入口文件并无增加,未超过默认的3。vue
也符合splitChunks
的拆分条件,单独打了一个包(2.js)
理解 chunks
chunks
用以告诉 splitChunks
的作用对象,其可选值有 async
、 initial
和 all
。默认值是 async
,也就是默认只选取异步加载的chunk进行代码拆分。这个我们在开头的例子里已经验证。这里我们通过两个例子来看一下当chunks的值为 initial
和 all
时,打包结果如何。
首先将chunks值改为 initial
:
chunks: "initial"
构建结果如下:
原因分析:
当 chunks
值为 initial
时,splitChunks
的作用范围变成了非异步加载的初始 chunk,例如我们的 index.js
就是初始化的时候就存在的chunk。而 vue 模块是在异步加载的chunk a.js
中引入的,所以并不会被分离出来。
chunks
仍使用 initial
, 我们对 index.js
和 a.js
稍作修改:
// index.js
import 'vue'
import('./a')
// a.js
console.log('a')
构建结果如下:
原因分析:
vue
在 index.js
直接被引入,而 index.js
是初始chunk,所以分离出来打到了 vendors~app.js
中。
能不能让 splitChunks
既处理初始chunk也处理异步chunk呢?答案是可以,只需要将 chunks
改为 all
:
chunks: "all"
对 index.js
和 a.js
稍作修改:
// index.js
import 'vue-router'
import('./a')
// a.js
import 'vue'
console.log('a')
构建结果如下:
原因分析:
chunks
值为 all
时,splitChunks
的处理范围包括了初始chunk和异步chunk两种场景,因此 index.js
中的 vue-router
被分拆到了 vendors~app.js
中,而异步加载的chunk a.js
中的 vue
被分拆到了 3.js
中。推荐在开发中将 chunks
设置为 all
。
理解 maxInitialRequests
maxIntialRequests
表示 splitChunks
在拆分chunk后,页面中需要请求的初始chunk数量不超过指定的值。所谓初始chunk,指的是页面渲染时,一开始就需要下载的js,区别于在页面加载完成后,通过异步加载的js。
对 splitChunks
做以下修改,其他使用默认配置:
chunks: 'initial',
maxInitialRequests: 1
对 index.js 稍作修改:
// index.js
import 'vue'
构建结果如下:
原因分析:
因为 maxInitialRequests
为1,如果 vue
从 index.js
中拆出的话,新创建的chunk作为初始chunk index.js
的前置依赖,是需要在页面初始化的时候就先请求的。那么初始化时的请求数变成了2,因此不满足拆分条件,所以 splitChunks
没有对 index.js
进行拆分。
理解 maxAsyncRequests
与 maxInitialRequests
相对,maxAsyncRequests
表示 splitChunks
在拆分chunk后,并行加载的异步 chunk 数不超过指定的值。
对 splitChunks
做以下修改,其他使用默认配置:
maxAsyncRequests: 1
对 index.js
稍作修改:
// index.js
import('./a')
// a.js
import 'vue'
console.log('a')
构建结果如下:
原因分析:
因为 maxAsyncRequests
为1,由于 a.js
是通过 import()
异步加载的,此时并行的异步请求数是1。如果将 vue
从 a.js
中拆出的话,拆出的包也将成为一个异步请求chunk。这样的话,当异步请求 a.js
的时候,并行请求数有2个。因此,不满足拆分条件,所以 splitChunks
没有对 a.js
进行拆分。
理解 minChunks
minChunks
表示一个模块至少应被指定个数的 chunk 所共享才能分割。默认为1。
对 splitChunks
做以下修改,其他使用默认配置:
chunks: 'all',
minChunks: 2
对 index.js
稍作修改:
// index.js
import 'vue'
构建结果如下:
原因分析:
因为 minChunks
为 2,所以只有当 vue
至少被2个 chunk 所共享时,才会被拆分出来。
思考题
请问如下代码,构建结果是什么?
chunks: 'all',
minChunks: 2
// index.js
import 'vue'
import './a'
// a.js
import 'vue'
console.log('a')
理解 cache groups
cacheGroups
继承 splitChunks
里的所有属性的值,如 chunks
、minSize
、minChunks
、maxAsyncRequests
、maxInitialRequests
、automaticNameDelimiter
、name
,我们还可以在 cacheGroups
中重新赋值,覆盖 splitChunks
的值。另外,还有一些属性只能在 cacheGroups
中使用:test
、priority
、reuseExistingChunk
。
通过 cacheGroups
,我们可以定义自定义 chunk 组,通过 test
条件对模块进行过滤,符合条件的模块分配到相同的组。
cacheGroups
有两个默认的组,一个是 vendors
,将所有来自 node_modules
目录的模块;一个 default
,包含了由两个以上的 chunk 所共享的模块。
前面的例子中,你可能注意到了怎么有的拆分出的chunk名字这么奇怪,例如 vendors~app
(默认由 cacheGroups
中组的 key + 源chunk名组成)。我们看一下如何自定义拆分出的chunk名。
首先找到该chunk所属的分组,该例为 vendors
分组,作如下修改,其他使用默认配置:
chunks:'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "customName",
priority: -10
}
}
对 index.js 稍作修改:
// index.js
import 'vue'
构建结果如下:
原因分析:
vue 来自 node_modules
目录,被分配到了默认的 vendors
组中,如果不指定 name
的话,会使用默认的chunk名,这里我们指定了 name
,因此最终的chunk名为customName
。
模块还可以分配到多个不同的组,但最终会根据 priority
优先级决定打包到哪个 chunk。
新增一个分组:
chunks:'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "customName",
priority: -10
},
customGroup: {
test: /[\\/]node_modules[\\/]/,
name: "customName1",
priority: 0
}
}
构建结果:
原因分析:
虽然 vendors
和 customGroup
这个两个组的条件都符合,但由于后者的优先级更高,所以最终将 vue
打包到了 customName1.js
中。
总结
讲解到这里,想必你对 webpack
如何进行代码分割有了深刻地理解了。对于文章开头的问题,可以给出你的答案了吧?
关注我们
推荐阅读
-
一种结构设计模式,允许在对象中动态添加新行为。它通过创建一个封装器来实现这一目的,即把对象放入一个装饰器类中,然后把这个装饰器类放入另一个装饰器类中,以此类推,形成一个封装器链。这样,我们就可以在不改变原始对象的情况下动态添加新行为或修改原始行为。 在 Java 中,实现装饰器设计模式的步骤如下: 定义一个接口或抽象类作为被装饰对象的基类。 公共接口 Component { void operation; } } 在本例中,我们定义了一个名为 Component 的接口,该接口包含一个名为 operation 的抽象方法,该方法定义了被装饰对象的基本行为。 定义一个实现基类方法的具体装饰对象。 公共类 ConcreteComponent 实现 Component { public class ConcreteComponent implements Component { @Override public void operation { System.out.println("ConcreteComponent is doing something...") ; } } 定义一个抽象装饰器类,该类继承于基类,并将装饰对象作为一个属性。 公共抽象类装饰器实现组件 { protected Component 组件 public Decorator(Component component) { this.component = component; } } @Override public void operation { component.operation; } } } 在这个示例中,我们定义了一个名为 Decorator 的抽象类,它继承了 Component 接口,并将被装饰对象作为一个属性。在操作方法中,我们调用了被装饰对象上的同名方法。 定义一个具体的装饰器类,继承自抽象装饰器类并实现增强逻辑。 公共类 ConcreteDecoratorA extends Decorator { public ConcreteDecoratorA(Component 组件) { super(component); } } public void operation { super.operation System.out.println("ConcreteDecoratorA 正在添加新行为......") ; } } 在本例中,我们定义了一个名为 ConcreteDecoratorA 的具体装饰器类,它继承自装饰器抽象类,并实现了操作方法的增强逻辑。在操作方法中,我们首先调用被装饰对象上的同名方法,然后添加新行为。 使用装饰器增强被装饰对象。 公共类 Main { public static void main(String args) { Component 组件 = new ConcreteComponent; component = new ConcreteDecoratorA(component); 组件操作 } } 在这个示例中,我们首先创建了一个被装饰对象 ConcreteComponent,然后通过 ConcreteDecoratorA 类创建了一个装饰器,并将被装饰对象作为参数传递。最后,调用装饰器的操作方法,实现对被装饰对象的增强。 使用场景 在 Java 中,装饰器模式被广泛使用,尤其是在 I/O 中。Java 中的 I/O 库使用装饰器模式实现了不同数据流之间的转换和增强。 让我们打开文件 a.txt,从中读取数据。InputStream 是一个抽象类,FileInputStream 是专门用于读取文件流的子类。BufferedInputStream 是一个支持缓存的数据读取类,可以提高数据读取的效率,具体代码如下: @Test public void testIO throws Exception { InputStream inputStream = new FileInputStream("C:/bbb/a.txt"); // 实现包装 inputStream = new BufferedInputStream(inputStream); byte bytes = new byte[1024]; int len; while((len = inputStream.read(bytes)) != -1){ System.out.println(new String(bytes, 0, len)); } } } } 其中 BufferedInputStream 对读取数据进行了增强。 这样看来,装饰器设计模式和代理模式似乎有点相似,接下来让我们讨论一下它们之间的区别。 第三,与代理模式的区别: 代理模式的目的是控制对对象的访问,它在对象外部提供一个代理对象来控制对原对象的访问。代理对象和原始对象通常实现相同的接口或继承相同的类,以确保两者可以相互替换。 装饰器模式的目的是动态增强对象的功能,而这是通过对象内部的包装器来实现的。在装饰器模式中,装饰器类和被装饰对象通常实现相同的接口或继承自相同的类,以确保两者可以相互替代。装饰器模式也被称为封装器模式。 在代理模式中,代理类附加了与原类无关的功能。
-
正负偏差变量 即 d2+、d2- 分别表示决策值中超出和未达到目标值的部分。而 di+、di- 均大于 0 刚性约束和目标约束(柔性目标约束有偏差) 在多目标规划中,>=/<= 在刚性约束中保持不变。当需要将约束条件转换为柔性约束条件时,需要将 >=/<= 更改为 =(因为已经有 d2+、d2- 用来表示正负偏差),并附加上 (+dii-di+) 注意这里是 +di、-di+!之所以是 +di,-di+,是因为需要将目标还原为最接近的原始刚性约束条件 优先级因素和权重因素 对多个目标进行优先排序和优先排序 目标规划的目标函数 是所有偏差变量的加权和。值得注意的是,这个加权和都取最小值。而 di+ 和 dii- 并不一定要出现在每个不同的需求层次中。具体分析需要具体问题具体分析 下面是一个例子: 题目中说设备 B 既要求充分利用,又要求尽可能不加班,那么列出的时间计量表达式即为:min z = P3 (d3- + d3 +) 使用 + 而不是 -d3 + 的原因是:正负偏差不可能同时存在,必须有 di+di=0 (因为判定值不可能同时大于目标值和小于目标值),而前面是 min,所以只要取 + 并让 di+ 和 dii- 都为正值即可。因此,得出以下规则: 最后,给出示例和相应的解法: 问题:某企业生产 A 和 B 两种产品,需要使用 A、B、C 三种设备。下表显示了与工时和设备使用限制有关的产品利润率。问该企业应如何组织生产以实现下列目标? (1) 力争利润目标不低于 1 500 美元; (2) 考虑到市场需求,A、B 两种产品的生产比例应尽量保持在 1:2; (3)设备 A 是贵重设备,严禁超时使用; (4)设备 C 可以适当加班,但要控制;设备 B 要求充分利用,但尽量不加班。 从重要性来看,设备 B 的重要性是设备 C 的三倍。 建立相应的目标规划模型并求解。 解:设企业生产 A、B 两种产品的件数分别为 x1、x2,并建立相应的目标计划模型: 以下为顺序求解法,利用 LINGO 求解: 1 级目标: 模型。 设置。 variable/1..2/:x;! s_con_num/1...4/:g,dplus,dminus;!所需软约束数量(g=dplus=dminus 数量)及相关参数; s_con(s_con_num);! s_con(s_con_num,variable):c;!软约束系数; 结束集 数据。 g=1500 0 16 15. c=200 300 2 -1 4 0 0 5; 结束数据 min=dminus(1);!第一个目标函数;!对应于 min=z 的第一小部分;! 2*x(1)+2*x(2)<12;!硬约束 @for(s_con_num(i):@sum(variable(j):c(i,j)*x(j))+dminus(i)-dplus(i)=g(i)); !使用设置完成的数据构建软约束表达式; ! !软约束表达式 @for(variable:@gin(x)); !将变量约束为整数; ! 结束 此时,第一级目标的最优值为 0,第一级偏差为 0: 第二级目标: !求 dminus(1)=0,然后求解第二级目标。 模型。 设置。 变量/1..2/:x;!设置:变量/1..2/:x; ! s_con_num/1...4/:g,dplus,dminus;!软约束数量及相关参数; s_con(s_con_num(s_con_num));! s_con(s_con_num,variable):c;! 软约束系数; s_con(s_con_num,variable):c;! 结束集 数据。 g=1500 0 16 15; c=200 300 2 -1 4 0 0 5; 结束数据 min=dminus(2)+dplus(2);!第二个目标函数 2*x(1)+2*x(2)<12;!硬约束 @for(s_con_num(i):@sum(variable(j):c(i,j)*x(j))+dminus(i)-dplus(i)=g(i)); ! 软约束表达式;! dminus(1)=0; !第一个目标结果 @for(variable:@gin(x)); ! 结束 此时,第二个目标的最优值为 0,偏差为 0: 第三目标 !求 dminus(2)=0,然后求解第三个目标。 模型。 设置。 变量/1..2/:x;!设置:变量/1..2/:x; ! s_con_num/1...4/:g,dplus,dminus;!软约束数量及相关参数; s_con(s_con_num(s_con_num));! s_con(s_con_num,variable):c;! 软约束系数; s_con(s_con_num,variable):c;! 结束集 数据。 g=1500 0 16 15; c=200 300 2 -1 4 0 0 5; 结束数据 min=3*dminus(3)+3*dplus(3)+dminus(4);!第三个目标函数。 2*x(1)+2*x(2)<12;!硬约束 @for(s_con_num(i):@sum(variable(j):c(i,j)*x(j))+dminus(i)-dplus(i)=g(i)); ! 软约束表达式;! dminus(1)=0; !第一个目标约束条件; ! dminus(2)+dplus(2)=0; !第二个目标约束条件 @for(variable:@gin(x));! 结束 最终结果为 x1=2,x2=4,dplus(1)=100,最优利润为
-
如何使用 splitChunks 对代码分割进行精细控制
-
南邮OJ Web任务大揭秘:层层挑战剖析 1. 挑战一:迷宫般的目录探索 题目作者似乎穷举了所有可能的目录组合,最终在404.php中的