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