Webpack 构建阶段:模块解析流程

25 阅读4分钟

一、流程概览

Webpack 的构建阶段核心任务是将入口文件及其依赖转换为模块依赖图。以下是关键步骤的源码解析及案例演示:

步骤核心对象源码关键文件作用
1. 入口模块处理EntryPluginEntryPlugin.js将配置的 entry 转换为编译入口
2. 模块加载NormalModuleFactoryNormalModuleFactory.js创建模块实例并初始化 Loader
3. 路径解析ResolverResolverFactory.js解析模块绝对路径及扩展名
4. AST 分析JavascriptParserJavascriptParser.js解析代码,提取 import/require
5. 递归处理子依赖CompilationCompilation.js构建完整的模块依赖图

二、分步源码解析

1. 入口模块处理:EntryPlugin 触发 compilation.addEntry

源码位置webpack/lib/EntryPlugin.js

// webpack/lib/EntryPlugin.js
class EntryPlugin {
  apply(compiler) {
    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      const { entry, options } = this;
      compilation.addEntry(
        compilation.options.context, // 上下文路径(如项目根目录)
        new EntryDependency(entry),  // 创建入口依赖对象
        options,
        (err) => {
          callback(err);
        }
      );
    });
  }
}

关键逻辑

  • 触发时机:Webpack 在 compiler.make 阶段调用 EntryPlugin
  • 入口转换:将配置的 entry(如 ./src/index.js)转换为 EntryDependency 对象。
  • 入口注册:调用 compilation.addEntry,将入口添加到编译流程。

2. 模块加载:NormalModuleFactory 创建模块对象

源码位置webpack/lib/NormalModuleFactory.js

// webpack/lib/NormalModuleFactory.js
class NormalModuleFactory {
  create(data, callback) {
    const resolveData = {
      context: data.context,
      request: dependency.request, // 模块请求路径(如 './a.js')
    };
    // 1. 解析模块路径
    this.hooks.resolve.callAsync(resolveData, (err, result) => {
      // 2. 创建模块实例
      const createdModule = new NormalModule({
        type: "javascript/auto",
        request: result.request, // 绝对路径(如 '/project/src/a.js')
        userRequest: dependency.userRequest,
        rawRequest: dependency.request,
        loaders: result.loaders, // 配置的 Loaders(如 ['babel-loader'])
      });
      callback(null, createdModule);
    });
  }
}

关键逻辑

  • 路径解析:调用 enhanced-resolve 解析模块绝对路径。
  • Loader 配置:合并模块对应的 Loader(如 .js 文件使用 babel-loader)。
  • 模块实例化:生成 NormalModule 对象,存储模块元信息。

3. 路径解析:Resolver 处理模块路径

源码位置webpack/lib/ResolverFactory.js(依赖 enhanced-resolve

// webpack/lib/ResolverFactory.js
const resolver = ResolverFactory.createResolver({
  fileSystem: compiler.inputFileSystem,
  extensions: [".js", ".json"], // 支持的扩展名
  alias: config.resolve.alias,  // 路径别名(如 '@' -> './src')
});

resolver.resolve({}, context, request, (err, resolvedPath) => {
  // resolvedPath 是模块的绝对路径(如 '/project/src/a.js')
});

关键逻辑

  • 路径转换:将相对路径(./a.js)解析为绝对路径。
  • 扩展名补全:自动补全 .js.json 等扩展名。
  • 别名处理:支持 Webpack 配置的 resolve.alias

4. AST 分析:Parser 解析代码并提取依赖

源码位置webpack/lib/javascript/JavascriptParser.js

// webpack/lib/javascript/JavascriptParser.js
class JavascriptParser {
  parse(source, state) {
    const ast = acorn.parse(source, { ecmaVersion: 2020 }); // 使用 acorn 生成 AST
    this.walkStatements(ast.body);
    return state;
  }

  walkImportDeclaration(statement) {
    const request = statement.source.value; // 提取 import 路径(如 './a.js')
    const dep = new ImportDependency(request, statement.range);
    state.current.addDependency(dep); // 将依赖添加到模块
  }
}

关键逻辑

  • AST 生成:使用 acorn 解析 JS 代码生成抽象语法树。
  • 依赖提取:遍历 AST 识别 import/require 语句,生成 ImportDependency 对象。
  • 依赖关联:将依赖添加到当前模块的 dependencies 数组。

5. 递归处理子依赖:构建完整依赖图

源码位置webpack/lib/Compilation.js

// webpack/lib/Compilation.js
class Compilation {
  buildModule(module, callback) {
    module.build(/* ... */, (err) => {
      // 模块构建完成后,处理其依赖
      this.processModuleDependencies(module, (err) => {
        // 递归处理子模块
        module.dependencies.forEach(dep => {
          const childModule = this.addModule(dep);
          this.buildModule(childModule, callback);
        });
      });
    });
  }
}

关键逻辑

  • 递归入口:在模块构建完成后,调用 processModuleDependencies
  • 子模块处理:遍历模块的 dependencies,对每个子模块重复 addModulebuildModule 流程。
  • 依赖图生成:最终形成一个树状的模块依赖图。

三、案例演示:从入口到依赖图的完整流程

1. 项目结构
src/
  index.js
  a.js
  b.js
2. 文件内容
// src/index.js
import a from './a.js';
console.log(a);

// src/a.js
import b from './b.js';
export default b + 1;

// src/b.js
export default 42;
3. 构建流程解析
步骤模块关键操作
1index.js- EntryPlugin 触发 addEntry
- 解析为绝对路径 /project/src/index.js
2index.js- NormalModuleFactory 创建模块实例
- 通过 Loader 处理代码(如有)
3index.js- Parser 解析 AST,提取 import './a.js'
4a.js- 递归调用 addModulebuildModule
- 解析 import './b.js'
5b.js- 继续递归处理,发现无更多依赖
6完成生成依赖图:index.js → a.js → b.js

四、关键流程图解

[EntryPlugin]
  ↓ 触发 compilation.addEntry
[Compilation]
  ↓ 调用 NormalModuleFactory.create()
[NormalModule]
  ↓ 使用 Resolver 解析路径
[enhanced-resolve]
  ↓ 返回绝对路径
[NormalModule.build()]
  ↓ 调用 Parser.parse() 提取依赖
[JavascriptParser]
  ↓ 生成 ImportDependency
[Compilation.processModuleDependencies()]
  ↓ 递归处理子模块

五、总结

Webpack 的构建阶段通过模块工厂、路径解析、AST 分析、递归处理四大核心机制,将入口文件逐步转换为完整的模块依赖图。理解这一流程有助于:

  1. 优化构建速度:减少不必要的模块解析(如缓存解析结果)。
  2. 调试依赖问题:通过分析 AST 和依赖关系定位错误。
  3. 自定义扩展:开发 Loader 或 Plugin 时,精准介入构建流程。