Webpack 学习笔记 —— ModuleGraph的构建过程

88 阅读7分钟

导读:这是一篇关于 Webpack ModuleGraph 构建过程的学习笔记。

在学习 Webpack 源码的过程中,发现 ModuleGraph 是一个非常核心的数据结构,它负责管理整个项目的模块依赖关系。这篇笔记主要记录了我在研究这部分源码时的心得,包括:

  • ModuleGraph 的设计思想和核心数据结构
  • 完整的依赖图构建流程
  • 一些关键实现细节的分析

笔记仅供参考,如有错误欢迎指出。

目录

ModuleGraph 概述

在 Webpack 的架构中,ModuleGraph 是连接 CompilationModule 的核心数据结构,它的主要职责是:

  1. 管理模块之间的依赖关系
  2. 提供模块的连接信息
  3. 支持代码分割和 Tree Shaking
  4. 协助热模块替换(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各自承担不同的职责:

  1. _moduleMap

    • 存储每个模块的所有连接关系
    • key是模块实例,value是ModuleGraphModule实例
    • ModuleGraphModule包含了模块的入度和出度连接
  2. _dependencyMap

    • 存储依赖和连接的映射关系
    • key是依赖实例,value是对应的Connection实例
    • 用于快速查找某个依赖对应的模块连接
  3. _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 引用的

这种引用关系对于:

  1. 模块路径解析
  2. 错误追踪
  3. 热更新
  4. 代码分割 都起着重要作用。

2. Module类

class Module {
	constructor() {
		this.dependencies = []; // 依赖数组
	}
}

Module类还有很多其他属性,目前我们只关心dependencies属性,它包含了当前模块的所有依赖关系。

3. Dependency类

Webpack中有多种类型的Dependency子类,用于处理不同类型的模块依赖:

  1. 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]
});
  1. CommonJsDependency(处理require)
// 源码:
const lodash = require("lodash");

// 对应的依赖实例:
new CommonJsDependency({
	request: "lodash",
	range: [16, 34],
	asiSafe: false
});
  1. EntryDependency(处理入口文件)
// webpack.config.js
module.exports = {
	entry: "./src/index.js"
};

// 对应的依赖实例:
new EntryDependency({
	request: "./src/index.js",
	name: "main" // 入口名称
});
  1. CssDependency(处理CSS模块)
// 源码:
import "./style.css";

// 对应的依赖实例:
new CssDependency({
	request: "./style.css",
	moduleType: "css",
	layer: undefined
});

每种Dependency都有其特定的属性和方法:

  1. 通用属性

    • request: 请求的模块路径
    • originModule: 创建依赖的模块
    • weak: 是否是弱依赖(可选加载)
    • optional: 是否是可选依赖(加载失败不报错)
  2. 特殊属性

    • 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的三个属性各自承担不同的职责:

  1. originModule(来源模块)

    • 表示依赖关系的来源模块
    • 对于入口文件,这个值是null
    • 对于其他模块,指向引用当前模块的模块
  2. dependency(依赖对象)

    • 保存了依赖的具体信息
    • 包含了依赖的类型(ImportDependency、CommonJsDependency等)
    • 记录了依赖在源码中的位置(range)
    • 存储了依赖的其他元数据
  3. 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中的应用:

  1. 模块关系表示
// 在_moduleMap中存储
moduleGraph._moduleMap.set(module, {
	incomingConnections: new Set([connection]),
	outgoingConnections: new Set()
});
  1. 依赖解析
// 在_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:

  1. EntryPlugin被触发
  2. 调用compilation.addEntry方法
  3. 将index.js作为初始依赖添加到moduleGraph中

具体流程:

addEntry
  └── addModuleTree
      └── handleModuleCreation
          ├── factorizeModule  // 创建模块
          └── addModule       // 添加到ModuleGraph

以下代码在handleModuleCreation中执行,对于 index.js 模块来说,会执行以下步骤:

  1. 创建模块对象

    • 根据资源路径创建NormalModule实例,经过之后的 build 过程,这个实例会保存模块的源代码、ast、sourcemap、依赖等信息
  2. 处理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 阶段,主要包含以下步骤:

  1. 读取源码

    // 通过 fs 读取 index.js 的内容
    const source = fs.readFileSync("./src/index.js", "utf-8");
    
  2. 执行 loader 链

    // 根据配置的 rules 找到匹配的 loader
    // 对于 index.js 会执行 babel-loader
    const result = babel.transform(source, {
    	presets: ["@babel/preset-react"]
    });
    
  3. 生成 AST

    // 使用 acorn 解析转换后的代码生成 AST
    const ast = acorn.parse(result.code, {
    	sourceType: "module",
    	ecmaVersion: 2020
    });
    
  4. 收集依赖

    // 遍历 AST,收集 import/require 语句
    const dependencies = [];
    walk(ast, {
    	ImportDeclaration(node) {
    		dependencies.push({
    			request: node.source.value
    		});
    	}
    });
    
  5. 保存信息

    module.dependencies = dependencies;
    module.ast = ast;
    module.source = source;
    

这样,index.js 模块就完成了 build 过程,Module 获得了:

  • 模块源码
  • AST 语法树
  • 依赖列表

下一步会用到module.dependencies来递归处理依赖。

  1. 递归处理依赖

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构建过程。每个模块都被正确地连接到它的依赖,形成了一个完整的依赖图。