导读:这是一篇关于 Webpack ModuleGraph 构建过程的学习笔记。
在学习 Webpack 源码的过程中,发现 ModuleGraph 是一个非常核心的数据结构,它负责管理整个项目的模块依赖关系。这篇笔记主要记录了我在研究这部分源码时的心得,包括:
- ModuleGraph 的设计思想和核心数据结构
- 完整的依赖图构建流程
- 一些关键实现细节的分析
笔记仅供参考,如有错误欢迎指出。
目录
ModuleGraph 概述
在 Webpack 的架构中,ModuleGraph 是连接 Compilation 和 Module 的核心数据结构,它的主要职责是:
- 管理模块之间的依赖关系
- 提供模块的连接信息
- 支持代码分割和 Tree Shaking
- 协助热模块替换(HMR)
整体流程如下:
graph TD
A[入口文件] --> B[解析模块]
B --> C[收集依赖]
C --> D[创建连接]
D --> E[递归处理]
E --> F[完整依赖图]
style A fill:#f9f,stroke:#333,stroke-width:4px
style F fill:#bbf,stroke:#333,stroke-width:4px
核心数据结构
ModuleGraph是Webpack中的核心数据结构,它用于表示模块之间的依赖关系。主要包含以下几个关键类:
1. ModuleGraph类
class ModuleGraph {
constructor() {
// 存储模块的所有连接
this._moduleMap = new Map();
// 存储依赖和连接的映射
this._dependencyMap = new Map();
// 存储模块的引用者关系
this._issuerMap = new Map();
}
}
ModuleGraph类中的三个核心Map各自承担不同的职责:
-
_moduleMap
- 存储每个模块的所有连接关系
- key是模块实例,value是ModuleGraphModule实例
- ModuleGraphModule包含了模块的入度和出度连接
-
_dependencyMap
- 存储依赖和连接的映射关系
- key是依赖实例,value是对应的Connection实例
- 用于快速查找某个依赖对应的模块连接
-
_issuerMap
- 存储模块的直接引用者(issuer)关系
- key是被引用的模块,value是引用该模块的模块
- 与_moduleMap的区别:
- _moduleMap存储了模块的所有连接关系
- _issuerMap只关注模块间的直接引用关系
- 主要用途:
- 用于生成模块的请求字符串(request string)
- 帮助确定模块的加载顺序
- 在错误追踪时提供模块引用链
- 用于某些插件的功能实现(如webpack-bundle-analyzer)
例如,对于以下代码:
// index.js
import { Button } from "./components/Button";
// components/Button.js
import { styles } from "./Button.css";
_issuerMap的结构会是:
Map {
'components/Button.js' => 'index.js',
'components/Button.css' => 'components/Button.js'
}
这样我们可以快速知道:
- Button.js 是被 index.js 引用的
- Button.css 是被 Button.js 引用的
这种引用关系对于:
- 模块路径解析
- 错误追踪
- 热更新
- 代码分割 都起着重要作用。
2. Module类
class Module {
constructor() {
this.dependencies = []; // 依赖数组
}
}
Module类还有很多其他属性,目前我们只关心dependencies属性,它包含了当前模块的所有依赖关系。
3. Dependency类
Webpack中有多种类型的Dependency子类,用于处理不同类型的模块依赖:
- ImportDependency(处理ES6 import)
// 源码:
import React from "react";
import "./style.css";
// 对应的依赖实例:
new ImportDependency({
request: "react", // 依赖的模块路径
originModule: "index.js", // 当前模块
range: [13, 26], // 源码中的位置
assertions: undefined // import断言
});
new ImportDependency({
request: "./style.css",
originModule: "index.js",
range: [27, 46]
});
- CommonJsDependency(处理require)
// 源码:
const lodash = require("lodash");
// 对应的依赖实例:
new CommonJsDependency({
request: "lodash",
range: [16, 34],
asiSafe: false
});
- EntryDependency(处理入口文件)
// webpack.config.js
module.exports = {
entry: "./src/index.js"
};
// 对应的依赖实例:
new EntryDependency({
request: "./src/index.js",
name: "main" // 入口名称
});
- CssDependency(处理CSS模块)
// 源码:
import "./style.css";
// 对应的依赖实例:
new CssDependency({
request: "./style.css",
moduleType: "css",
layer: undefined
});
每种Dependency都有其特定的属性和方法:
-
通用属性
- request: 请求的模块路径
- originModule: 创建依赖的模块
- weak: 是否是弱依赖(可选加载)
- optional: 是否是可选依赖(加载失败不报错)
-
特殊属性
- range: 在源码中的位置范围
- assertions: import断言信息
- asiSafe: ASI(自动分号插入)安全标记
- recursive: 是否递归处理
- regExp: 匹配模式
4. Connection类
Connection(连接)是ModuleGraph中最基础的数据结构,它描述了模块之间的具体连接关系。
class ModuleGraphConnection {
constructor(originModule, dependency, module) {
this.originModule = originModule; // 来源模块
this.dependency = dependency; // 依赖对象
this.module = module; // 目标模块
}
}
Connection的三个属性各自承担不同的职责:
-
originModule(来源模块)
- 表示依赖关系的来源模块
- 对于入口文件,这个值是null
- 对于其他模块,指向引用当前模块的模块
-
dependency(依赖对象)
- 保存了依赖的具体信息
- 包含了依赖的类型(ImportDependency、CommonJsDependency等)
- 记录了依赖在源码中的位置(range)
- 存储了依赖的其他元数据
-
module(目标模块)
- 表示被依赖的模块
- 是一个Module实例
- 包含了模块的完整信息(源码、AST等)
举例说明:
// index.js
import React from "react";
// 对应的Connection实例
const connection = new ModuleGraphConnection(
indexModule, // originModule: index.js的Module实例
new ImportDependency({
// dependency: import语句的依赖对象
request: "react",
range: [7, 20]
}),
reactModule // module: react模块的Module实例
);
Connection在ModuleGraph中的应用:
- 模块关系表示
// 在_moduleMap中存储
moduleGraph._moduleMap.set(module, {
incomingConnections: new Set([connection]),
outgoingConnections: new Set()
});
- 依赖解析
// 在_dependencyMap中存储
moduleGraph._dependencyMap.set(dependency, connection);
5. ModuleGraphModule类
ModuleGraphModule 是对模块连接关系的封装,包含两个重要的属性:
-
incomingConnections: 表示当前模块的入度连接(被引用)
- 记录了所有依赖当前模块的其他模块
- 用于分析模块的引用关系
- 可以帮助确定哪些模块依赖于当前模块
- 在代码分割和Tree Shaking时很有用
-
outgoingConnections: 表示当前模块的出度连接(引用的其他模块)
- 记录了当前模块依赖的其他所有模块
- 用于追踪模块的依赖关系
- 可以帮助确定当前模块依赖了哪些其他模块
- 在模块加载顺序分析和循环依赖检测时很重要
示例项目
让我们通过一个具体的React项目来理解ModuleGraph的构建过程。
项目配置
webpack.config.js:
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].[contenthash].js"
},
module: {
rules: [
{
test: /\.js$/,
use: "babel-loader",
exclude: /node_modules/
},
{
test: /\.css$/,
use: ["style-loader", "css-loader"]
}
]
}
};
项目文件
入口文件 src/index.js:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "./styles/main.css";
ReactDOM.render(<App />, document.getElementById("root"));
src/App.js:
import React from "react";
import Header from "./components/Header";
import Footer from "./components/Footer";
const App = () => (
<div>
<Header />
<h1>Welcome</h1>
<Footer />
</div>
);
export default App;
构建流程详解
让我们通过这个示例项目来看ModuleGraph是如何一步步构建的:
1. 入口处理
当Webpack开始构建时,首先处理入口文件index.js:
- EntryPlugin被触发
- 调用compilation.addEntry方法
- 将index.js作为初始依赖添加到moduleGraph中
具体流程:
addEntry
└── addModuleTree
└── handleModuleCreation
├── factorizeModule // 创建模块
└── addModule // 添加到ModuleGraph
以下代码在handleModuleCreation中执行,对于 index.js 模块来说,会执行以下步骤:
-
创建模块对象
- 根据资源路径创建NormalModule实例,经过之后的 build 过程,这个实例会保存模块的源代码、ast、sourcemap、依赖等信息
-
处理ModuleGraph
// 建立依赖模块的连接关系 moduleGraph.setResolvedModule(oriModule, dependency, module);对于index.js来说,oriModule(父模块)为null,Module就是自己,执行后ModuleGraph结构如下:
ModuleGraph ├── _moduleMap │ └── "index.js" -> ModuleGraphModule { │ incomingConnections: Set([ │ Connection { │ originModule: null, │ module: NormalModule("index.js"), │ dependency: EntryDependency │ } │ ]) │ } └── _dependencyMap └── EntryDependency -> Connection因为 index.js 是入口文件,oriModule 为 null,所以不会处理 oriModule,如果是非入口文件,则需要处理ModuleGraphModule.outgoingConnections。
这一步的关键代码如下:
class ModuleGraph {
// 建立模块之间的连接
setResolvedModule(originModule, dependency, module) {
// 创建新的连接
const connection = new ModuleGraphConnection(
originModule,
dependency,
module
);
const connections = this._getModuleGraphModule(module).incomingConnections;
connections.add(connection);
if (originModule) {
const mgm = this._getModuleGraphModule(originModule);
if (mgm.outgoingConnections === undefined) {
mgm.outgoingConnections = new SortableSet();
}
mgm.outgoingConnections.add(connection);
} else {
this._dependencyMap.set(dependency, connection);
}
}
}
index.js 的 build 过程
当 index.js 模块被创建后,会进入 build 阶段,主要包含以下步骤:
-
读取源码
// 通过 fs 读取 index.js 的内容 const source = fs.readFileSync("./src/index.js", "utf-8"); -
执行 loader 链
// 根据配置的 rules 找到匹配的 loader // 对于 index.js 会执行 babel-loader const result = babel.transform(source, { presets: ["@babel/preset-react"] }); -
生成 AST
// 使用 acorn 解析转换后的代码生成 AST const ast = acorn.parse(result.code, { sourceType: "module", ecmaVersion: 2020 }); -
收集依赖
// 遍历 AST,收集 import/require 语句 const dependencies = []; walk(ast, { ImportDeclaration(node) { dependencies.push({ request: node.source.value }); } }); -
保存信息
module.dependencies = dependencies; module.ast = ast; module.source = source;
这样,index.js 模块就完成了 build 过程,Module 获得了:
- 模块源码
- AST 语法树
- 依赖列表
下一步会用到module.dependencies来递归处理依赖。
- 递归处理依赖
index.js 构建完成之后,会对每个 Dependency 都会递归执行 handleModuleCreation 处理完所有依赖后,ModuleGraph结构变成:
ModuleGraph
├── \_moduleMap
│ ├── "index.js" -> ModuleGraphModule {
│ │ incomingConnections: Set([...]),
│ │ outgoingConnections: Set([...])
│ },
│ ├── "react" -> ModuleGraphModule {
│ │ incomingConnections: Set([
│ │ Connection { index.js -> react },
│ │ Connection { App.js -> react }
│ │ ])
│ },
│ ├── "react-dom" -> ModuleGraphModule {
│ │ incomingConnections: Set([
│ │ Connection { index.js -> react-dom }
│ │ ])
│ },
│ ├── "App.js" -> ModuleGraphModule {
│ │ incomingConnections: Set([
│ │ Connection { index.js -> App.js }
│ │ ]),
│ │ outgoingConnections: Set([
│ │ Connection { App.js -> react },
│ │ Connection { App.js -> ./components/Header },
│ │ Connection { App.js -> ./styles/App.css }
│ │ ])
│ },
│ ├── "./components/Header.js" -> ModuleGraphModule {
│ │ incomingConnections: Set([
│ │ Connection { App.js -> Header.js }
│ │ ]),
│ │ outgoingConnections: Set([
│ │ Connection { Header.js -> react },
│ │ Connection { Header.js -> ./styles/Header.css }
│ │ ])
│ },
│ ├── "./styles/main.css" -> ModuleGraphModule {
│ │ incomingConnections: Set([
│ │ Connection { index.js -> main.css }
│ │ ])
│ },
│ ├── "./styles/App.css" -> ModuleGraphModule {
│ │ incomingConnections: Set([
│ │ Connection { App.js -> App.css }
│ │ ])
│ },
│ └── "./styles/Header.css" -> ModuleGraphModule {
│ │ incomingConnections: Set([
│ │ Connection { Header.js -> Header.css }
│ │ ])
│ }
这样,我们就完成了从入口文件开始的整个ModuleGraph构建过程。每个模块都被正确地连接到它的依赖,形成了一个完整的依赖图。