Webpack Compilation 源码解析

177 阅读3分钟

Webpack 的 Compilation 对象是构建过程中的核心上下文,它管理单次编译的完整生命周期。下面从多个维度深入解析其工作原理:


一、Compilation 的定位

  1. 编译上下文
    类似一次构建的"沙盒环境",保存了本次编译的所有中间状态(模块、依赖、Chunk、资源等)。

  2. 与 Compiler 的关系

    • Compiler:全局管理器,贯穿整个 Webpack 生命周期(watch 模式下存活)。
    • Compilation:单次构建的临时容器,每次触发构建(包括 watch 模式下的重新构建)都会创建新实例。

二、核心生命周期流程

graph TD
  A[创建 Compilation] --> B[启动构建]
  B --> C[解析入口文件]
  C --> D[递归构建模块树]
  D --> E[生成 Chunk]
  E --> F[优化阶段]
  F --> G[生成最终资源]
  G --> H[结束编译]

三、关键职责分解

1. 模块管理

  • 模块收集
    从入口文件出发,递归解析所有依赖,形成模块树。每个模块包含:

    • 源码内容
    • 依赖列表(dependencies
    • Loader 处理后的结果
    • 哈希值(用于缓存)
  • 模块生命周期方法

    class Compilation {
      // 添加新模块
      addModule(module) { /*...*/ }
    
      // 触发模块构建(调用 loader)
      buildModule(module, callback) { /*...*/ }
    
      // 处理模块依赖
      processModuleDependencies(module) { /*...*/ }
    }
    

2. 依赖图构建

  • 依赖解析算法
    通过 enhanced-resolve 库处理模块路径,支持别名、扩展名省略等特性。

  • 循环依赖处理
    记录已解析模块,避免重复构建:

    const moduleMap = new Map();
    function resolveModule(request) {
      if (moduleMap.has(request)) {
        return moduleMap.get(request);
      }
      // 新建模块并存入 Map
    }
    

3. Chunk 生成策略

  • 入口点分割
    每个入口文件生成独立 Chunk。

  • 动态导入切割
    import() 语法触发新 Chunk 创建。

  • 优化拆分
    通过 SplitChunksPlugin 实现:

    // webpack.config.js
    optimization: {
      splitChunks: {
        chunks: 'all'
      }
    }
    

4. 资源生成

  • Chunk → 文件映射
    使用 Template 子系统将 Chunk 转为最终代码:
    compilation.createChunkAssets(() => {
      chunks.forEach(chunk => {
        const template = chunk.hasRuntime() 
          ? MainTemplate 
          : ChunkTemplate;
        const source = template.render(chunk);
        compilation.emitAsset(
          chunk.name + '.js',
          new RawSource(source)
        );
      });
    });
    

5. 优化阶段

  • Tree Shaking
    通过 sideEffects 标记和 Terser 联动删除未使用代码。

  • Scope Hoisting
    合并模块作用域,减少闭包数量:

    new webpack.optimize.ModuleConcatenationPlugin()
    
  • 代码压缩
    调用 TerserWebpackPlugin 进行混淆优化。


四、核心数据结构

1. 模块存储

class Compilation {
  constructor() {
    this.modules = new Set();       // 所有模块
    this.chunks = new Set();        // 所有 Chunk
    this.assets = {};               // 输出资源列表
    this.entries = [];              // 入口模块集合
  }
}

2. Chunk 结构

class Chunk {
  constructor(name) {
    this.name = name;              // 通常对应入口名
    this.files = [];               // 输出文件名集合
    this.modules = new Set();      // 包含的模块
    this.entryModule = undefined;  // 入口模块(仅入口 Chunk 存在)
  }
}

五、插件交互机制

1. 关键钩子示例

compilation.hooks: {
  buildModule: SyncHook,           // 开始构建模块
  succeedModule: SyncHook,         // 模块构建成功
  finishModules: AsyncSeriesHook,  // 所有模块构建完成
  optimize: SyncHook,              // 开始优化
  optimizeChunks: SyncHook,        // Chunk 优化阶段
  afterOptimizeAssets: SyncHook,   // 资源优化后
}

2. 插件开发示例

实现一个统计模块数量的插件:

class ModuleCountPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('ModuleCount', (compilation) => {
      compilation.hooks.finishModules.tap('ModuleCount', (modules) => {
        console.log(`Total modules: ${modules.size}`);
      });
    });
  }
}

六、性能优化设计

1. 增量构建

  • 基于 fileDependenciescontextDependencies 实现:
    compilation.fileDependencies.add(filePath);
    compilation.contextDependencies.add(contextDir);
    

2. 缓存策略

  • 模块级别的缓存(Module.buildInfo
  • 文件系统快照(Snapshot
  • 持久化缓存(cache: { type: 'filesystem' }

七、典型应用场景

  1. 自定义资源处理
    通过 emit 钩子添加 LICENSE 文件:

    compilation.hooks.emit.tap('AddLicense', () => {
      compilation.assets['LICENSE.txt'] = {
        source: () => 'MIT License',
        size: () => 10
      };
    });
    
  2. 动态修改入口

    compilation.hooks.addEntry.tap('DynamicEntry', (entry) => {
      if (entry.name === 'main') {
        entry.dependencies.push(new DynamicEntryDependency('./polyfill.js'));
      }
    });
    

总结

Compilation 是 Webpack 构建过程的核心指挥官,负责:

  • 🧩 模块的解析与依赖管理
  • 🧬 Chunk 的生成与优化
  • 📦 最终资源的组装
  • 🔌 插件系统的执行上下文

理解其内部机制,能帮助开发者更好地进行性能调优和定制化插件开发。