Webpack5源码解读系列2 - 构建应用模块拓扑图
概念
Any time one file depends on another, webpack treats this as a dependency. This allows webpack to take non-code assets, such as images or web fonts, and also provide them as dependencies for your application. When webpack processes your application, it starts from a list of modules defined on the command line or in its configuration file. Starting from these entry points, webpack recursively builds a dependency graph that includes every module your application needs, then bundles all of those modules into a small number of bundles - often, only one - to be loaded by the browser.
上面这段官方描述我们可以提炼出以下信息:在Webpack
中,一个文件依赖了另外一个文称之为dependency
(依赖),文件可以是代码资源或者非代码资源。在开始编译时,Webpack
会从入口开始递归遍历项目,获取所有模块的依赖,构建一个dependency graph
(依赖拓扑图),进而根据这些依赖打包项目并用于浏览器运行。
在Webpack
中每个文件被视为一个模块,应用中以Module
领域模型描述,而模块之间的引用(即依赖)由Dependency
领域模型描述,Webpack
应用构建时遍历Module
及其依赖,从而构建出项目文件拓扑图ModuleGraph
(Webpack5
之前使用DependencyGraph
描述项目文件拓扑)。这里可以先行列出领域模型概念:
Module
Module
是文件在内存中的模型映射,描述文件位置、唯一标识、依赖以及后续代码生成等,同时Module
是一个基础类,不同类型模块通过继承可按需实现模块能力,如NormalModule
描述项目文件,RuntimeModule
描述运行时能力。
Dependency
Module
使用了导入或者导出语法时,会被解析为Dependency
,描述Module
之间的引用关系,同时Dependency
也是基类,通过继承可实现不同的导入导出语法,如cjs、amd、es6模块导入导出均需要实现对应Dependency
实现类。
ModuleGraph
从项目入口文件开始解析节点(Module
)和边(Dependency
)构成一张拓扑图,并最终组装成为ModuleGraph
。
模块分析过程
入口插件
Webpack
通过插件机制极大提高了构建工具灵活性,编译开始时通过EntryPlugin
往编译器执行队列中添加入口依赖:
class EntryPlugin {
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory }) => {
// 设置 EntryDependency -> 模块构建工厂函数,确定Dependency变成什么实例
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
// make阶段开始时,往构建任务注入入口
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
}
static createDependency(entry, options) {
// 将入口配置处理成 EntryDependency
const dep = new EntryDependency(entry);
dep.loc = { name: typeof options === "object" ? options.name : options };
return dep;
}
}
为什么Webpack
不将入口Dependency
创建逻辑放到构建任务中,而是将其通过插件机制解耦出去呢?这是因为入口配置除了可以被EntryPlugin
视为普通入口,还可以通过使用DllPlugin
处理为动态链接库,所以这里由插件完成入口处理逻辑。
模块解析
模块解析过程分为模块创建、模块构建、依赖获取三个阶段:
-
模块创建
Dependency
(即依赖)分为很多类型,在代码上以继承Dependency
类实现,常用类型有:
ModuleDependency
:处理模块导入依赖,具体为模块导入依赖语法;ContextDependency
:处理模块目录上下,如require.context
语法读取目录内容;NullDependency
:空依赖类型,处理模块导出;
模块解析过程中需要递归遍历模块所有依赖,所以使用到了ModuleDependency
,但并不是直接使用ModuleDependency
,而是通过其实现类完成领域模型建立(没错ModuleDependency
只是中间基类)。这是因为Webpack
需要处理包含esm
、cjs
、amd
在内多种模块管理能力,需要为各种模块处理语法预设解析逻辑,所以需要分别为其编写实现类,所以继承关系如下:
Webpack
进入make
阶段之后,入口插件会向Compilation
插入多一个入口依赖,这便是应用编译的开始。拿到Dependency
之后Webpack
会获取到Dependency
对应的模块创建工厂函数,如果是普通代码文件,那么便是NormalModuleFactory
。这些Dependency
和工厂函数的映射维护在Compilation
中,在初始化时由各个插件注册维护,可参考上面的入口插件一段代码:
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
获取到模块创建工厂函数之后,通过工厂函数创建模块实例,具体模块创建流程如下:
-
模块构建
类似于Dependency
,模块也存在很多类型,如普通文件资源处理为NormalModule
、Webpack
运行时代码资源处理为RuntimeModule
、远程模块处理为RemoteModule
。以NormalModule
为例子,模块构建时调用NormalModule
的build
方法,构建时需要经历文件读取、文件预处理、文件解析三个阶段:
- 文件读取:读取文件代码内容;
- 文件预处理:使用
loader
转换文件内容,如转换js语法、将非js文件转为js可识别内容; - 文件解析:使用解析器将文件内容解析为抽象语法树,如js模块使用JS解析器、css模块使用CSS解析器、WASM模块使用WASM解析器;
NormalModule
构建有两个目的:
- 文件内容预处理:使用loader处理语法糖、特殊文件内容;
- 语法转译:文件解析成抽象语法树之后,通过遍历抽象语法树并抛出事件,由插件捕获并在代码生成阶段做语法转译操作。
-
模块依赖获取
解析器解析模块代码时会不断抛出事件,各种模块语法插件通过监听模块导入事件从而创建出本模块所有Dependency
,如下面模块语法会解析出多个Dependency
:
模块构建完毕后,已经确定了本模块以及相关联的依赖,此时会以Module
映射为ModuleGraphModule
的节点,Dependency
为ModuleGraphConnection
的边填充ModuleGraph
,逐步构建模块拓扑图:
填充完ModuleGraph
之后,会遍历本模块的Dependency
进入下一轮模块解析。
构建流程
模块解析这一段讲解了一个模块从依赖到模块的构建过程,是一个递归构建任务过程,由于在模块解析过程中存在需要文件读写操作,递归构建任务时性能会非常低,所以在真实场景中Webpack
使用了AsyncQueue
允许多个任务并发处理,从而提高构建速度。
AsynQueue介绍
Webpack
提供了很多种异步任务执行模型,如AsyncQueue
、AsyncTree
,为异步场景提供任务执行调度能力,AsyncQueue
简单理解为带优先级别、限制任务数量异步任务执行队列。在构建阶段使用了factoriesModule
、addModule
、buildModule
、processModuleDependencies
四个异步队列,分别为模块创建、模块添加到队列、模块构建、解析模块依赖提供执行队列。
先介绍AsyncQueue
内部执行机制:
- 子队列:子队列优先级别低于父队列,当父队列为空时才会将子队列内容取出执行,即存在优先级别;
parallelism
:提供并发执行数量;
四个队列的优先级别分别按照factoriesModule
->addModule
->buildModule
->processModuleDependencies
从高顺序顺序排列,这样处理的用意是什么呢?
首先看一下AsyncQueue
的遍历模式:图数据结构一般使用DFS和BFS遍历方式,AsyncQueue
可以理解为优化的BFS,我们以普通的图遍历来看:
按照普通BFS,那么遍历顺序是[A, B, C, D, E]
,如果遍历到C
时需要花费大量时间,那么需要等待。而使用AsyncQueue
时,如果C
花费大量时间,那么会先处理D
和E
,这是使用AsyncQueue
的好处。增加优先级级别目的是在大多数情况下尽可能让四个 队列 都处于工作状态,由于遍历是从根节点开始,一般情况下越靠近根节点,那么层级就越高,而越高层级的节点处理完毕后产生的子树节点就会越多。
递归构建过程
在基于上述模块解析、AsyncQueue
队列执行过程我们可以得出下面的构建流程图:
case解析
我们以一个简单例子解析一下构建结果:
构建顺序如下:
- EntryPlugin调用addEntry,构建模块A;
- 获得模块A的依赖,将B、C模块推入构建队列中,等待下一轮的构建
- 以此类推完成D、E模块构建
构建完成后,ModuleGraph会有如下数据:
const ModuleGraph = {
// Module -> ModuleGraphModule
_moduleMap: {
[Module<A>]: {
inComingConnections: Set([
// ModuleGraphConnection<OriginModule, Module>,OriginModule为边起点节点,Module为边的终点节点
ModuleGraphConnection<null, A>
]),
outGoingCnnections: Set([
ModuleGraphConnection<A, B>,
ModuleGraphConnection<A, C>
])
},
[Module<B>]: {
inComingConnections: Set([
ModuleGraphConnection<A, B>
]),
outGoingCnnections: Set([
ModuleGraphConnection<B, E>,
])
},
[Module<C>]: {
inComingConnections: Set([
ModuleGraphConnection<A, C>
]),
outGoingCnnections: Set([
ModuleGraphConnection<C, D>,
])
},
[Module<D>]: {
inComingConnections: Set([
ModuleGraphConnection<C, D>
]),
outGoingCnnections: Set([
ModuleGraphConnection<D, E>,
])
},
[Module<E>]: {
inComingConnections: Set([
ModuleGraphConnection<B, E>,
ModuleGraphConnection<D, E>
]),
outGoingCnnections: Set([])
},
}
}
小结
本文介绍了几个关键领域模型:
-
Module
:文件/资源在Webpack
中的映射,存在多种类型的Module
,如:- 普通文件资源处理为
NormalModule
; Webpack
运行时代码资源处理为RuntimeModule
;- 远程模块处理为
RemoteModule
;
- 普通文件资源处理为
-
Dependency
:模块依赖的描述,主要存在三种类型:ModuleDependency
:处理模块导入依赖,具体为模块导入依赖语法;ContextDependency
:处理模块目录上下,如require.context
语法读取目录内容;NullDependency
:空依赖类型,处理模块导出;
-
ModuleGraph
:项目文件引用关系在Webpack
中的映射拓扑图,内部由ModuleGraphModule
、ModuleGraphConnection
组成:ModuleGraphModule
:Module
在ModuleGraph
的映射;ModuleGraphConnection
:Dependency
在ModuleGraph
的映射;
本文还介绍了项目模块构建流程,使用AsyncQueue
提高构建速度。
转载自:https://juejin.cn/post/7231805092272373815