Webpack5源码解读系列3 - 分析产物组织形式
概念
Webpack在make阶段构建了文件依赖关系的映射ModuleGraph,接下来进入seal阶段,seal意为“密封”,即将模块代码封装起来。而在封装之前,需要明确产物文件组织形式,Webpack通过ModuleGraph分析出产物加载顺序,从而得到产物文件引用关系图ChunkGraph,并最终用于Chunk文件输出。
在ChunkGraph构建流程之前,需要先浅介绍一下几个领域模型:
Chunk
Chunk是模块的封装,内部存放多个模块。Chunk可以与输出文件画上等号,一个Chunk最终会输出为一个产物文件,
ChunkGroup
ChunkGroup可以是一个或者多个Chunk,用于维护Chunk的父子关系(Chunk的父子关系决定了最后的加载顺序)。
ChunkGraph
ChunkGroup存在父子关系,并最后形成一张DAG,由ChunkGraph维护。ChunkGraph为后续代码生成提供查询能力。
构建过程
ChunkGroup分包规则
在了解ChunkGraph构建过程之前,需要前置了解ChunkGroup的生成规则,其中可以分为三种生成规则(分包规则),分别是Entry分包、异步分包、Runtime分包。
Entry分包
Webpack入口配置会生成EntryPoint,EntryPoint继承于ChunkGroup,专门处理入口维度的ChunkGroup,下面统一将EntryPoint称为ChunkGroup。入口可以配置同步或者异步入口:
module.exports = {
entry: {
entry1: {
import: 'A.js',
},
entry2: {
import: 'B.js',
// 依赖于 entry1 入口加载,是一异步模块加载
dependOn: ['entry1']
}
}
}
配置entry字段中配置的每个入口都会处理为一个EntryPoint,如果存在dependOn字段,那么会处理为一个具有父子关系的EntryPoint。上述入口配置最终会生成两个Chunk。
异步分包
在代码中使用require.ensure('xxx')或者import('xxx')模块都能够使用代码分割能力,代码分割能力底层都是将目标代码从本Chunk分离并成为一个独立Chunk,在运行时异步加载代码。
import './Home.js';
// 使用异步加载能力,会将 target.js模块从本模块所属chunk独立出去
import('./target.js');
异步加载能力在处理时会创建新的ChunkGroup并与引用方的ChunkGroup形成父子关系,父子关系在文件输出时体现为加载顺序。
Runtime分包
默认地,Webpack应用运行时代码都会放到应用入口Chunk上,这是为了能够让所有代码都能够使用运行时能力。如果一个项目存在多个入口,那么可以选择将Runtime相关能力独立出来,减少代码冗余:
module.exports = {
entry: {
entry1: {
import: 'A.js',
runtime: 'runtime'
},
entry2: {
import: 'B.js',
runtime: 'runtime',
}
}
}
上面配置编译时,会产生三个ChunkGroup,分别是entry1、entry2、runtime,编译产物中runtime.js文件同时被entry1.js和entry2.js引用。
ChunkGraph构建
遍历模块
从入口开始,借助ModuleGraph遍历应用模块拓扑图,此操作有两个目的:
- 绑定模块与
ChunkGroup,而ChunkGroup内部可以包含多个Chunk,实际上是绑定了模块与Chunk的关系; - 获取每个模块的异步导入语法,创建新的
ChunkGroup以及异步模块对应的Chunk;
从入口开始,获取入口模块,并将其推入队列中,接下来对于执行队列中每个元素都做以下操作:
- 从队列中取出模块,将模块和对应的
ChunkGroup绑定起来,此操作同时绑定了模块和Chunk,之后将本模块所导入的所有子模块推进执行队列中等待执行; - 如果模块存在
blocks属性(blocks代表本模块所使用的异步模块),那么会解析block并根据block内容生成ChunkGroup,此时此时将block指向的入口Module推进队列中,等待遍历;

ChunkGroup关系处理
有两种方式决定ChunkGroup关系:
- 入口配置设置
dependOn属性表明一个EntryPoint依赖于哪些入口,由此决定父子关系; - 异步引入会产生一个
ChunkGroup,此ChunkGroup会与原ChunkGroup绑定父子关系;
ChunkGroup父子关系代表着ChunkGroup所影响模块加载顺序,ChunkGroup绑定父子关系之后会形成树状结构,后者加载依赖于前者加载完成,如下面的case:

上述例子中由于ChunkGroup2和ChunkGroup3包含了同样的Module(Chunk也相同),所以最终只会产生一份文件,如果将ChunkGroup2、ChunkGroup3进行合并会形成一张有向无环图(DAG),代表着运行时文件加载顺序:

冗余模块去除
最小可用模块集合作用
将ModuleGraph遍历完毕后,Webpack已经创建了项目中可能存在的所有ChunkGroup,此时可能存在冗余的ChunkGroup,如果不做精简,那么会造成生成代码冗余。冗余来源于重复模块代码,如存在以下模块引用关系,ChunkGroup1和ChunkGroup2同时共享B模块,此时B模块会存在两个产物副本:

此时需要做一些优化,将模块B从ChunkGroup2去除,在ChunkGroup1保留,这样做不会影响到运行时表现。
为什么是从 ChunkGroup2 中移除而不是从 ChunkGroup1 中移除呢?
ChunkGroup最终会形成一张有向无环图(DAG),应用初始化加载时会以其中某个拓扑排序顺序加载,也就是说ChunkGroup2对应的Chunk一定会先于ChunkGroup1对应Chunk加载,所以将B模块归属于ChunkGroup1能够保证应用所有地方都能访问到B模块。构建过程使用了计算最小可用集合算法去除冗余。
计算最小可用模块集合
最小可用集合算法实现起来很复杂,但是能用一句话概括:某个ChunkGroup的最小可用集合是从入口到其节点的必经之路所包含的所有模块。举一个例子说明一下:

上面是由ChunkGroup组成的DAG,从图中我们能够得出每个ChunkGroup的最小可用模块集合:
| 名称 | 最小可用模块集合 | 解释 |
|---|---|---|
| ChunkGroup1 | 无 | |
| ChunkGroup2 | A | |
| ChunkGroup3 | A | |
| ChunkGroup4 | A、B | ChunkGroup4是由ChunkGroup2异步导入,ChunkGroup1和ChunkGroup2都是其必经之路 |
| ChunkGroup5 | A、C | ChunkGroup4是由ChunkGroup3异步导入,ChunkGroup1和ChunkGroup3都是其必经之路 |
| ChunkGroup6 | A、D | 对于ChunkGroup6来说,只有ChunkGroup1、ChunkGroup4和ChunkGroup5是必经之路(ChunkGroup4和ChunkGroup5具有相同的Chunk,后续会处理成一个ChunkGroup) |
在极端情况下可能一个ChunkGroup的所有模块都会被清除,此时ChunkGroup也会连带被清除,下图中ChunkGroup1包含了ChunkGroup2所有模块,通过最小可用集合可以得出ChunkGroup2并不包含任何模块,最终会将ChunkGroup2清除:

小结
分析产物组织形式需要关注两方面内容:
-
ChunkGroup分包规则:
- Entry分包:使用多个入口配置最终会产生多个文件
- 异步分包:使用异步导入产生多一个ChunkGroup
- Runtime分包:使用
runtime字段将runtime代码单独分包
-
ChunkGraph构建
ChunkGraph通过遍历ModuleGraph从而获取到所有ChunkGroup,并通过建立父子关系从而构建ChunkGraph,构建完毕后还会通过计算最小可用模块集合的方式去除冗余模块。
转载自:https://juejin.cn/post/7231805092272980023