概念来源:Dependency Graph | webpack
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, just one - to be loaded by the browser.
大意:Webpack 处理应用代码时,会从开发者提供的 entry 开始递归地组建起包含所有模块的 Dependency Graph,之后再将这些 module 打包为 bundles。
然而事实远不止官网描述的这么简单。Dependency Graph 贯穿 Webpack 整个运行周期——从「构建阶段」的模块解析,到「生成阶段」的 Chunk 生成,以及 Tree-shaking、HMR 等功能都高度依赖于它。它是 Webpack 资源构建流程中最核心的数据结构。
在正式介绍数据结构之前,先回顾 Webpack 构建阶段的关键步骤:
这个过程从 entry 模块开始,逐步递归找出所有依赖文件,模块之间隐式形成了:
这就是一张有向图(CJS 循环引用会形成环,多数场景无环)——也就是 Webpack 官方所称的 Dependency Graph。
Webpack 5 之前,依赖关系隐含在 DependenciesBlock / Module 对象的属性中:
// ❌ 旧设计 (Webpack 4.x)
class Module extends DependenciesBlock {
// 问题1: 依赖关系耦合在 Module 内部
dependencies: Dependency[]; // 该模块依赖了谁
issuer: Module | null; // 谁引用了我(单向引用)
// 问题2: 模块搜索算法与资源构建逻辑混在一起
// 问题3: 同一 Module 无法在多个 Graph 之间共享
}这种设计存在三大问题:
| 问题 | 说明 | 影响 |
|---|---|---|
| 职责混乱 | 模块关系管理 + 资源构建逻辑耦合在同一个 Class | Module 类复杂度极高,难以维护 |
| 关系隐晦 | 依赖关系散落在多处属性中 | 开发者/插件作者难以理解模块拓扑 |
| 无法复用 | Module 对象持有强引用 | 同一 Module 无法跨 Compilation 共享 |
Webpack 5 对此进行了架构级重构:
核心思想:将依赖关系从
Module/Dependency类型中解耦抽离,以独立的 Graph 数据结构记录模块间关系,并基于原生Map/Set实现更高效的搜索、校验、遍历算法。
Webpack 5 的 Dependency Graph 由以下核心类型协作构成:
ModuleGraph —— 依赖图的容器// webpack/lib/ModuleGraph.js (简化)
class ModuleGraph {
constructor() {
/** Dependency → Connection 映射 */
this._dependencyMap = new Map();
/** Module → 衍生信息 映射 */
this._moduleMap = new Map();
}
}关键属性:
| 属性 | 类型 | 作用 |
|---|---|---|
_dependencyMap | Map<Dependency, ModuleGraphConnection> | 根据 Dependency 快速找到连接信息(谁引用了谁、为什么引用) |
_moduleMap | Map<Module, ModuleGraphModule> | 根据 Module 找到其衍生信息(入边、出边、导出信息) |
ModuleGraphConnection —— 单条连接每条连接代表一个依赖关系,包含完整的上下文:
class ModuleGraphConnection {
constructor(originModule, dependency, module, explanation, weak, condition) {
/** 引用发起者(父模块),入口模块为 null */
this.originModule = originModule;
/** 产生这个连接的原因(如 import 语句) */
this.dependency = dependency;
/** 被引用的目标(子模块) */
this.module = module;
/** 连接说明(如 "import" 等描述文本) */
this.explanation = explanation;
/** 是否为弱依赖(不影响 chunk 包含判断) */
this.weak = weak;
/** 条件加载(如动态 import()) */
this.condition = condition;
}
}ModuleGraphModule —— 模块的衍生信息class ModuleGraphModule {
constructor() {
/** 谁引用了我?(入边集合) */
this.incomingConnections = undefined;
/** 我引用了谁?(出边集合) */
this.outgoingConnections = undefined;
/** 导出信息(Tree-shaking 核心) */
this.exportsInfo = new ExportsInfo();
}
}对于如下简单的依赖关系:
index.js ──import──▶ a.js
index.js ──import──▶ b.js
a.js ──require──▶ c.js在 ModuleGraph 中会形成如下结构:
本质上,_moduleMap 形成了一个有向图:
Webpack 5 支持多种 Module 子类,每种对应不同的资源处理策略:
| Module 类型 | 典型场景 | 示例 |
|---|---|---|
| NormalModule | 最常用,JS/CSS/HTML 文件 | import './style.css' |
| RawModule | 外部注入的源码,无需 AST 解析 | Plugin 动态创建 |
| ContextModule | 动态路径表达式 | require('./locale/' + lang) |
| DelegatedModule | Module Federation 远程模块 | remotes: { app: 'app@http://' } |
| CssModule | CSS Modules / MiniCssExtractPlugin | .module.css |
| AssetModule | Asset Modules (v5 新增) | import logo from './logo.png' |
| JsonModule | JSON 文件(v5 内置支持) | import data from './data.json' |
| RuntimeModule | Webpack 运行时辅助函数 | __webpack_require__ 等 |
Dependency 是产生连接的原因,不同语法对应不同的 Dependency 子类:
| Dependency 类型 | 触发语法 | 模块系统 |
|---|---|---|
HarmonyImportSideEffectDependency | import foo from './foo' | ESM |
HarmonyExportSpecifierDependency | export const foo = 1 | ESM |
CommonJsRequireDependency | const foo = require('./foo') | CJS |
CommonJsExportsDependency | module.exports = { ... } | CJS |
CssImportDependency | @import './style.css' | CSS |
UrlDependency | url('./img.png') | CSS |
AssetDependency | import img from './img.png' | Asset |
EntryDependency | entry: './index.js' | Config |
ModuleHotAcceptDependency | module.hot.accept(...) | HMR |
ConstDependency | 编译时变量替换 | Internal |
依赖关系主要在构建阶段的两个关键节点被收集到 ModuleGraph 中:
// webpack/lib/ModuleGraph.js (v5.107 精简版)
class ModuleGraph {
constructor() {
this._dependencyMap = new Map();
this._moduleMap = new Map();
}
/**
* 记录一条已解析的模块依赖关系
*
* @param {Module} originModule - 引用发起者(父模块)
* @param {Dependency} dependency - 依赖对象(原因)
* @param {Module} module - 被引用的目标(子模块)
*/
setResolvedModule(originModule, dependency, module) {
// 1. 创建连接对象
const connection = new ModuleGraphConnection(
originModule,
dependency,
module,
undefined,
dependency.weak,
dependency.getCondition(this)
);
// 2. 记录到 _dependencyMap:dep → connection
this._dependencyMap.set(dependency, connection);
// 3. 更新目标模块的入边(谁引用了我)
const targetMgm = this._getModuleGraphModule(module);
if (targetMgm.incomingConnections === undefined) {
targetMgm.incomingConnections = new Set();
}
targetMgm.incomingConnections.add(connection);
// 4. 更新源模块的出边(我引用了谁)
if (originModule) {
const sourceMgm = this._getModuleGraphModule(originModule);
if (sourceMgm.outgoingConnections === undefined) {
sourceMgm.outgoingConnections = new Set();
}
sourceMgm.outgoingConnections.add(connection);
}
}
_getModuleGraphModule(module) {
let mgm = this._moduleMap.get(module);
if (mgm === undefined) {
mgm = new ModuleGraphModule(module);
this._moduleMap.set(module, mgm);
}
return mgm;
}
}| 方法 | 签名 | 返回值 | 用途 |
|---|---|---|---|
getConnection | (dep: Dependency) | Connection|null | 获取某依赖对应的连接 |
getModule | (dep: Dependency) | Module|undefined | 获取某依赖解析到的模块 |
getResolvedModule | (origin, dep) | Module|undefined | 获取特定连接的目标模块 |
getOutgoingConnections | (module: Module) | Connection[] | 获取模块的所有出边 |
getIncomingConnections | (module: Module) | Connection[] | 获取模块的所有入边 |
getIssuer | (module: Module) | Module|null | 获取直接引用者 |
getExportsInfo | (module: Module) | ExportsInfo | 获取导出信息(Tree-shaking) |
updateModule | (dep: Dependency, module: Module) | void | 将某依赖解析到的目标模块替换为新模块(见示例 4) |
const plugin = {
apply(compiler) {
compiler.hooks.thisCompilation.tap('MyPlugin', (compilation) => {
compilation.hooks.processAssets.tap(
{
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
},
() => {
const moduleGraph = compilation.moduleGraph;
for (const [module, mgm] of moduleGraph._moduleMap) {
const outgoing = moduleGraph.getOutgoingConnections(module);
console.log(`📦 ${module.identifier()}:`);
for (const conn of outgoing) {
console.log(` └─→ ${conn.module.identifier()} `
+ `[原因: ${conn.dependency.constructor.name}]`);
}
}
}
);
});
}
};输出示例:
📦 ./src/index.js:
└─→ ./src/a.js [原因: HarmonyImportSideEffectDependency]
└─→ ./src/b.js [原因: HarmonyImportSideEffectDependency]
📦 ./src/a.js:
└─→ ./src/utils.js [原因: CommonJsRequireDependency]function findWhoImports(compilation, targetModuleIdent) {
const moduleGraph = compilation.moduleGraph;
for (const [module, mgm] of moduleGraph._moduleMap) {
if (!mgm.incomingConnections) continue;
for (const conn of mgm.incomingConnections) {
if (conn.module.identifier() === targetModuleIdent) {
const importer = conn.originModule
? conn.originModule.identifier()
: '(entry)';
const reason = conn.dependency.constructor.name;
console.log(`${targetModuleIdent} 被 ${importer} 通过 ${reason} 引用`);
}
}
}
}
// 用法:查找 lodash 被哪些模块引入
findWhoImports(compilation, 'lodash');function analyzeExportsInfo(compilation) {
const moduleGraph = compilation.moduleGraph;
for (const [module] of moduleGraph._moduleMap) {
const exportsInfo = moduleGraph.getExportsInfo(module);
console.log(`\n📤 ${module.identifier()} 的导出情况:`);
for (const [exportName, exportInfo] of exportsInfo._exports) {
const used = exportInfo.getUsed(undefined); // undefined = 不限定使用者
const usedName = exportInfo.getUsedName(undefined);
console.log(
` ${exportName}: ` +
`${used ? '✅ 已使用' : '⚪ 未使用'} ` +
(usedName ? `→ 别名: "${usedName}"` : '')
);
}
}
}输出示例:
📤 ./src/utils.js 的导出情况:
add: ✅ 已使用 → 别名: "add"
subtract: ⚪ 未使用
multiply: ⚪ 未使用
helper: ✅ 已使用 → 别名: "helperFn"const { NormalModule } = require('webpack');
class RewriteImportPlugin {
apply(compiler) {
compiler.hooks.compilation.tap('RewriteImport', (compilation) => {
compilation.hooks.succeedModule.tap('RewriteImport', (module) => {
if (!(module instanceof NormalModule)) return;
const moduleGraph = compilation.moduleGraph;
for (const dep of module.dependencies) {
if (
dep.userRequest &&
dep.userRequest.includes('old-library')
) {
// 将该依赖解析到的目标模块替换为新模块
// (真实签名 updateModule(dependency, module),SideEffectsFlagPlugin 即此用法)
const newModule = /* ... 获取或创建新模块 ... */;
moduleGraph.updateModule(dep, newModule);
console.log(`🔄 重写依赖: ${dep.userRequest}`);
}
}
});
});
}
}当开发服务器开启 Hot Module Replacement 时,文件变更后需要增量重建受影响的模块,而非全量重建:
📌 注意:每次(重)编译都会创建新的 Compilation 与 ModuleGraph 实例,未变更的模块从缓存中恢复并重新接入图结构。ModuleGraph 本身并没有"标记整图为脏"的
invalidate()API。
| API | 作用范围 | 性能影响 | 使用场景 |
|---|---|---|---|
compilation.rebuildModule(module) | 单个模块(重建 + 刷新连接) | 较小(局部重建) | HMR 单文件修改 |
moduleGraph.updateModule(dep, module) | 单条连接 | 极小 | 插件把某依赖的解析目标替换为新模块(如 SideEffectsFlagPlugin) |
// webpack/lib/ModuleGraph.js (v5.107 精简版)
class ModuleGraph {
/**
* 将某条依赖解析到的目标模块替换为新模块
*
* @param {Dependency} dependency - 引用方的依赖对象
* @param {Module} module - 新的目标模块
*/
updateModule(dependency, module) {
const connection = this.getConnection(dependency);
if (connection.module === module) return;
// 1. 克隆旧连接并把目标指向新模块
const newConnection = connection.clone();
newConnection.module = module;
this._dependencyMap.set(dependency, newConnection);
// 2. 旧连接置为非激活(不再参与优化判断)
connection.setActive(false);
// 3. 把新连接挂到源模块的出边与目标模块的入边
const originMgm = this._getModuleGraphModule(connection.originModule);
originMgm.outgoingConnections.add(newConnection);
const targetMgm = this._getModuleGraphModule(module);
targetMgm.incomingConnections.add(newConnection);
}
}进入「生成阶段」后,Webpack 会将 ModuleGraph 中的依赖关系转化为 ChunkGraph:
两者的区别:
| 维度 | ModuleGraph | ChunkGraph |
|---|---|---|
| 关注点 | 模块间的代码依赖关系 | 模块与输出 Chunk 的归属关系 |
| 边的含义 | import/require(为什么依赖) | 属于哪个打包产物 |
| 使用阶段 | 构建 + 生成阶段均活跃 | 主要在 seal 阶段使用 |
| 典型操作 | getModule / getIssuer | getModuleChunks / isModuleInChunk |
💡 提示:ChunkGraph 的详细内容将在下一章《ChunkGraph:如何管理模块与 Chunk 的映射?》中深入讲解。
src/
├── index.js # entry
├── a.js # ESM 模块
├── b.js # CJS 模块
└── utils/
└── math.js # 工具函数// index.js
import { add } from './a';
const b = require('./b');
console.log(add(1, 2), b.value);
// a.js
export function add(x, y) {
const { multiply } = require('./utils/math');
return x + y; // multiply 未使用 → 可被 tree-shaking
}
// b.js
module.exports = { value: 42 };
// utils/math.js
export function multiply(x, y) { return x * y; }
export function divide(x, y) { return x / y; }{
// ========== _dependencyMap: 所有的边 ==========
_dependencyMap: Map(6) {
// 入口连接
[EntryDependency("./src/index.js")] → Connection {
originModule: null,
module: NormalModule("./src/index.js"),
dependency: EntryDependency("./src/index.js")
},
// index → a (ESM import)
[HarmonyImportSideEffectDependency("./src/a.js")] → Connection {
originModule: NormalModule("./src/index.js"),
module: NormalModule("./src/a.js"),
dependency: HarmonyImportSideEffectDependency("./src/a.js"),
weak: false
},
// index → b (CJS require)
[CommonJsRequireDependency("./src/b.js")] → Connection {
originModule: NormalModule("./src/index.js"),
module: NormalModule("./src/b.js"),
dependency: CommonJsRequireDependency("./src/b.js")
},
// a → math (CJS require)
[CommonJsRequireDependency("./src/utils/math.js")] → Connection {
originModule: NormalModule("./src/a.js"),
module: NormalModule("./src/utils/math.js"),
dependency: CommonJsRequireDependency("./src/utils/math.js")
},
// math.js 自身的导出依赖(用于导出跟踪)
[HarmonyExportSpecifierDependency("multiply")] → Connection { ... },
[HarmonyExportSpecifierDependency("divide")] → Connection { ... }
},
// ========== _moduleMap: 所有的节点及衍生信息 ==========
_moduleMap: Map(4) {
[NormalModule("./src/index.js")] → ModuleGraphModule({
incomingConnections: Set([
Connection{ originModule: null } // 入口
]),
outgoingConnections: Set([
Connection{ module: "./src/a.js" }, // ESM import
Connection{ module: "./src/b.js" } // CJS require
]),
exportsInfo: ExportsInfo({ /* index 无命名导出 */ })
}),
[NormalModule("./src/a.js")] → ModuleGraphModule({
incomingConnections: Set([
Connection{ originModule: "./src/index.js" }
]),
outgoingConnections: Set([
Connection{ module: "./src/utils/math.js" } // require
]),
exportsInfo: ExportsInfo({
exports: Map {
"add" → ExportInfo({ used: true, usedName: "add" })
}
})
}),
[NormalModule("./src/b.js")] → ModuleGraphModule({
incomingConnections: Set([Connection{ originModule: "./src/index.js" }]),
outgoingConnections: undefined, // 无下游依赖
exportsInfo: ExportsInfo({ exports: Map {} })
}),
[NormalModule("./src/utils/math.js")] → ModuleGraphModule({
incomingConnections: Set([Connection{ originModule: "./src/a.js" }]),
outgoingConnections: undefined,
exportsInfo: ExportsInfo({
exports: Map {
"multiply" → ExportInfo({ used: false }), // ⚠️ 可被 tree-shaking
"divide" → ExportInfo({ used: false }) // ⚠️ 可被 tree-shaking
}
})
})
}
}通过 ModuleGraph 可以得出:
index → a → math 和 index → bmath.multiply 和 math.divide(Tree-shaking 候选)| 特性 | Webpack 4 (DependenciesBlock) | Webpack 5 (ModuleGraph) |
|---|---|---|
| 存储位置 | module.dependencies[] | 独立的 ModuleGraph 实例 |
| 反向索引 | module.issuer (单一引用) | incomingConnections (多引用集合) |
| 搜索效率 | O(n) 遍历 | O(1) Map/Set 查找 |
| 共享能力 | 无法跨 Compilation 共享 | 天然支持(Graph 独立于 Module) |
| 增量更新 | 不支持 | invalidate() + update(module) |
| 扩展性 | 修改需改动 Module 类 | 只需扩展 Graph 方法 |
ModuleGraph API 查询模块依赖关系,实现精准的自定义优化import 和 CJS require 引用,ModuleGraph 中会创建几个 Connection?它们的 Dependency 类型分别是什么?invalidate() 和 update(module) 分别适合什么场景?选择不当会有什么后果?exportsInfo 判断某个导出是否被 Tree-shaking 移除?下一章预告:《ChunkGraph:如何管理模块与 Chunk 的映射?》将深入讲解 ModuleGraph 的依赖关系如何在 seal 阶段转化为 ChunkGraph,以及 Code Splitting 的具体实现原理。