# 模块依赖关系
如何从 Entry 开始逐步递归读入、解析模块内容,并最终构建出模块依赖关系图 —— ModuleGraph 对象。为什么需要先将依赖文件构建为 Dependency,之后再根据 Dependency 创建文件对应的 Module 对象?Dependency 对象到底有什么作用?
Dependency Graph 贯穿 Webpack 整个运行周期,从「构建阶段」的模块解析,到「生成阶段」的 Chunk 生成,以及 Tree-shaking 等功能都高度依赖于Dependency Graph ,是 Webpack 资源构建流程中一个非常核心的数据结构。
# Dependency Graph 是什么
回顾 Webpack 构建阶段的关键过程:
- 首先根据 entry 配置信息创建若干 EntryDependency 对象
- 调用 NormalModuleFactory ,根据 EntryDependency 对象的资源路径创建 Module 子类对象
- 将 Module 代码解析为 AST 结构
- 遍历 AST,找到所有模块导入语句(import/require)
- 根据导入语句创建对应的 Dependency 子类对象
- 递归执行步骤 2,直到所有项目文件都处理完毕。
这个过程从 entry 模块开始,逐步递归找出所有依赖文件,模块之间隐式形成了以 entry 为起点,以模块为节点,以导入导出依赖为边的有向图关系 - Dependency Graph 模块依赖关系图。
这个 Dependency Graph 是 Webpack 内部非常重要的过程信息之一,后续封装 Chunk、Code Splits、Tree-Shaking、Hot Module Replacement 等等,几乎所有功能都需要依赖这一信息实现。
Webpack5 之前,Dependency Graph 关系隐含在 Dependence、Module 对象的一系列属性中,例如:
- 通过 module.dependencies 数组记录模块依赖对象;
- 通过 dependency.module 记录依赖对应的模块对象引用;
- 通过 module.issuer 记录父模块引用。
这种设计存在很多问题,例如:模块之间的关系非常隐晦难懂;模块搜索算法与模块资源构建逻辑耦合在同一个 Class 实体内,Module 职责不单一且复杂度非常高;同一个 Module 对象无法在多个 Dependency Graph 之间共享,等等。为此,Webpack5 重构 了 Dependency Graph 的具体实现,将依赖关系从 Dependence/Module 类型中解耦出来,以一套独立的 Graph 数据结构记录模块间依赖关系,并基于 Map/Set 等原生模块实现更高效的模块搜索、校验、遍历算法。
# Dependency Graph 设计
Webpack 5.0 之后的 Dependency Graph 涉及如下数据类型:
ModuleGraph
:记录 Dependency Graph 信息的容器,记录构建过程中涉及到的所有module
、dependency
对象,以及这些对象互相之间的引用;ModuleGraphConnection
:记录模块间引用关系的数据结构,内部通过originModule
属性记录引用关系中的父模块,通过module
属性记录子模块;ModuleGraphModule
:Module
对象在 Dependency Graph 体系下的补充信息,包含模块对象的incomingConnections
—— 指向模块本身的ModuleGraphConnection
集合,即谁引用了模块自身;outgoingConnections
—— 该模块对外的依赖,即该模块引用了其他那些模块。
这些类型之间关系的基本逻辑是:
Compilation
类内部会维护一个全局唯一的ModuleGraph
实例对象- 每次解析出新模块后,将 Module、Dependency,以及模块之间的关系 ——
ModuleConnection
记录到compilation.moduleGraph
对象中 ModuleGraph
除了记录依赖关系外,还提供了许多工具方法,方便使用者迅速读取出module
或dependency
附加的信息。ModuleGraph
内部有两个关键属性:- 通过
_dependencyMap
属性记录Dependency
对象与ModuleGraphConnection
连接对象之间的映射关系,后续的处理中可以基于这层映射迅速找到Dependency
实例对应的引用与被引用者; - 通过
_moduleMap
属性记录Module
与ModuleGraphModule
之间的映射关系。
- 通过
最终,通过 ModuleGraph
、ModuleGraphConnection
、ModuleGraphModule
三种类型的协作,在主体的 Module
、Dependency
体系之外,记录模块之间的依赖信息。
依赖关系收集过程,主要发生在构建阶段的两个节点:
addDependency
:webpack 从模块内容中解析出引用关系后,创建适当的Dependency
子类并调用该方法记录到module
实例;handleModuleCreation
:模块解析完毕后,webpack 遍历父模块的依赖集合,调用该方法创建Dependency
对应的子模块对象,之后调用moduleGraph.setResolvedModule
方法将父子引用信息记录到moduleGraph
对象上。moduleGraph.setResolvedModule 方法的逻辑大致为:
class ModuleGraph {
constructor() {
/** @type {Map<Dependency, ModuleGraphConnection>} */
this._dependencyMap = new Map();
/** @type {Map<Module, ModuleGraphModule>} */
this._moduleMap = new Map();
}
/**
* @param {Module} originModule the referencing module
* @param {Dependency} dependency the referencing dependency
* @param {Module} module the referenced module
* @returns {void}
*/
setResolvedModule(originModule, dependency, module) {
const connection = new ModuleGraphConnection(
originModule,
dependency,
module,
undefined,
dependency.weak,
dependency.getCondition(this)
);
this._dependencyMap.set(dependency, connection);
const connections = this._getModuleGraphModule(module).incomingConnections;
connections.add(connection);
const mgm = this._getModuleGraphModule(originModule);
if (mgm.outgoingConnections === undefined) {
mgm.outgoingConnections = new Set();
}
mgm.outgoingConnections.add(connection);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- 主要更改了
_dependencyMap
及moduleGraphModule
的出入connections
属性,以此收集当前模块的上下游依赖关系。
# 案例分析
案例:index.js 依赖 a.js 和 b.js,那么其构建过程中模块间依赖关系生成过程如下:
- 首先创建
index.js
对应的EntryDependency
对象; - 调用
NormalModuleFactory
创建EntryDependency
对应的NormalModule
实例; - 执行
compilation.handleModuleCreation
,经过解析、遍历 AST 等操作,最终得到a.js
、b.js
两个新 Dependency 对象; - 调用
NormalModuleFactory
为这两个 Dependency 对象创建对应的NormalModule
对象; - 调用
moduleGraph.setResolvedModule
记录entry
模块与a/b
模块的依赖关系。
最终生成如下数据结果:
ModuleGraph: {
_dependencyMap: Map(3){
{
EntryDependency{request: "./src/index.js"} => ModuleGraphConnection{
module: NormalModule{request: "./src/index.js"},
// 入口模块没有引用者,故设置为 null
originModule: null
}
},
{
HarmonyImportSideEffectDependency{request: "./src/a.js"} => ModuleGraphConnection{
module: NormalModule{request: "./src/a.js"},
originModule: NormalModule{request: "./src/index.js"}
}
},
{
HarmonyImportSideEffectDependency{request: "./src/a.js"} => ModuleGraphConnection{
module: NormalModule{request: "./src/b.js"},
originModule: NormalModule{request: "./src/index.js"}
}
}
},
_moduleMap: Map(3){
NormalModule{request: "./src/index.js"} => ModuleGraphModule{
incomingConnections: Set(1) [
// entry 模块,对应 originModule 为null
ModuleGraphConnection{ module: NormalModule{request: "./src/index.js"}, originModule:null }
],
outgoingConnections: Set(2) [
// 从 index 指向 a 模块
ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} },
// 从 index 指向 b 模块
ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} }
]
},
NormalModule{request: "./src/a.js"} => ModuleGraphModule{
incomingConnections: Set(1) [
ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} }
],
// a 模块没有其他依赖,故 outgoingConnections 属性值为 undefined
outgoingConnections: undefined
},
NormalModule{request: "./src/b.js"} => ModuleGraphModule{
incomingConnections: Set(1) [
ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} }
],
// b 模块没有其他依赖,故 outgoingConnections 属性值为 undefined
outgoingConnections: undefined
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
从上面的 Dependency Graph 可以看出,本质上 ModuleGraph._moduleMap
已经形成了一个有向无环图结构,其中字典 _moduleMap
的 key 为图的节点,对应 value ModuleGraphModule
结构中的 outgoingConnections
属性为图的边,则上例中从起点 index.js
出发沿 outgoingConnections
向前可遍历出图的所有顶点。
# Dependency Graph 作用
- 信息索引
- 辅助构建 ChunkGraph
信息索引是 ModuleGraph 最重要的功能,在 ModuleGraph
类型中提供了很多实现 module
/ dependency
信息查询的工具函数,例如:
getModule(dep: Dependency)
:根据dep
查找对应的module
实例。getOutgoingConnections(module)
:查找module
实例的所有依赖。getIssuer(module: Module)
:查找module
在何处被引用。- ...
Webpack5 内部的许多插件、Dependency 子类、Module 子类的实现都需要用到这些工具函数查找特定模块、依赖的信息,例如:
SplitChunksPlugin
在优化chunks
处理中,需要使用moduleGraph.getExportsInfo
查询各个模块的exportsInfo
(模块导出的信息集合,与 tree-shaking 强相关)信息以确定如何分离 chunk。- 在
compilation.seal
函数中,需要遍历entry
对应的 dep 并调用moduleGraph.getModule
获取完整的module
定义。 - ...
此外,在 Webpack 完成模块构建,进入「生成阶段」之后,会按一系列规则将模块逐一分配到不同 Chunk 对象中,在 Webpack4 时,这个过程主要围绕 Chunk
及 ChunkGroup
两个类型展开。
而 5.0 之后,对 Chunk 之间的依赖关系管理也做了一次大型 重构 (opens new window):首先根据默认规则为每一个 entry
创建对应 Chunk 对象 ,之后调用 buildChunkGraph
方法,遍历 moduleGraph
对象,找出入口模块对应的所有 Module
对象,并将依赖关系转化为 ChunkGraph 对象
# 小结
综上,Webpack 构建过程中会持续收集模块之间的引用、被引用关系,并记录到 Dependency Graph 结构中,后续的 Chunk 封装、Code Split、Tree-Shaking 等,但凡需要分析模块关系的功能都强依赖于 Dependency Graph。
可以说,Dependency Graph 是 Webpack 底层最关键的模块地图数据,因此在 Webpack5 之后,Dependency Graph 结构被解耦抽离为以 ModuleGraph
为中心的若干独立类型,架构逻辑更合理,模块搜索、分析效率也得到不同程度优化,进而使得 Webpack5 构建速度也有明显提升。