Webpack 打包流程及 Hooks 汇总

259 阅读7分钟

工作原理概述

graph TD
初始化参数 --> 开始编译 
开始编译 --> 确定入口 
确定入口 --> 编译模块
编译模块 --> 完成编译模块
完成编译模块 --> 输出资源
输出资源 --> 输出完成

单次执行流程如上图所示。在监听模式下,流程如下:

graph TD
  初始化-->编译;
  编译-->输出;
  输出-->文件发生变化
  文件发生变化-->编译

Webpack 打包流程详解

Webpack 构建流程可分为以下三大阶段:

  1. 初始化阶段:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  2. 编译阶段:从 Entry 出发,针对每个 Module 串行调用对应的 Loader 翻译文件内容,再递归处理该 Module 依赖的其他 Module
  3. 输出阶段:将编译后的 Module 组合成 Chunk,将 Chunk 转换为文件 assets,输出到文件系统

核心概念

  • Chunks:代码分割单元,包含:

    • 模块依赖关系图
    • 运行时逻辑
    • 分组策略(通过 splitChunks 配置)
    {
      id: "chunkId",
      files: ["main.js"], // 关联的assets
      _modules: [/* 模块列表 */],
      runtime: "runtime逻辑"
    }
    
  • Assets:输出资源,包含:

    • 经过 Loader 处理的文件内容
    • 压缩/优化后的代码
    • 带 hash 的文件名
    {
      "main.js": {
        source() { return "压缩后的代码" },
        size() { return 文件大小 }
      }
    }
    

Plugin Hooks

可以通过 tap 方法为对应的 hook 注册回调函数,这些函数存储在 taps 属性中。

  1. 同步 Hooks(如 compilation):

    • 使用 .tap() 注册的回调会在 hook 触发时立即执行
    • 执行顺序遵循注册顺序
    • 类型:
      • SyncHook:顺序执行所有注册的回调函数,忽略返回值
      • SyncBailHook:顺序执行所有注册的回调函数,当回调函数返回值不为undefined时,立即停止后续回调函数的执行,如 entryOption。这样当某个插件已经确定了最终配置时,可以跳过剩余插件的执行
      // 多个插件处理 entry 配置
      compiler.hooks.entryOption.tap('PluginA', () => {
        if (conditionA) return customEntry; // 条件满足时直接返回
      });
      
      compiler.hooks.entryOption.tap('PluginB', () => {
        // 如果 PluginA 已返回,这里不会执行
      });
      
  2. 异步 Hooks(如 emit):

    • AsyncSeriesHook:串行执行,插件回调按注册顺序依次执行。每个回调必须显式调用 callback() 或返回Promise。前一个回调完成后才会执行下一个。
    • AsyncParallelHook:所有注册的回调同时触发,不保证执行顺序。需要每个回调显式调用 callback() 或返回Promise。
// taps 属性结构
compiler.hooks.environment.taps = [
  {
    name: 'MyPlugin',  // 插件名称
    type: 'sync',     // 钩子类型
    fn: Function,     // 回调函数
    stage: 0,         // 执行优先级(仅异步 Hook 有效)
    before: undefined // 指定执行顺序
  }
]

// 指定执行顺序
compiler.hooks.environment.tap({
  name: 'MyPlugin',
  stage: 100,        // 数字越大执行越晚
  before: 'OtherPlugin' // 在指定插件前执行
}, () => { /*...*/ });

Compile 流程及对应 Hooks

1. 初始化阶段

const webpack = (options, callback) => {
  // 一、初始化参数
  // 1.1 参数校验
  const webpackOptionsValidationErrors = validateSchema(
    webpackOptionsSchema,
    options
  );
  if (webpackOptionsValidationErrors.length) {
    throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
  }
  
  // 1.2 初始化 compiler
  let compiler;
  if (Array.isArray(options)) {
    // 多配置处理
    compiler = new MultiCompiler(
      Array.from(options).map(options => webpack(options))
    );
  } else if (typeof options === "object") {
    options = new WebpackOptionsDefaulter().process(options);

    compiler = new Compiler(options.context);
    compiler.options = options;
    
    // 应用 Node 环境插件
    new NodeEnvironmentPlugin({
      infrastructureLogging: options.infrastructureLogging
    }).apply(compiler);
    
    // 1.3 加载用户自定义插件
    if (options.plugins && Array.isArray(options.plugins)) {
      for (const plugin of options.plugins) {
        if (typeof plugin === "function") {
          plugin.call(compiler, compiler);
        } else {
          plugin.apply(compiler);
        }
      }
    }
    
    // 触发环境钩子
    compiler.hooks.environment.call();
    compiler.hooks.afterEnvironment.call();
    
    // 注册内部插件
    compiler.options = new WebpackOptionsApply().process(options, compiler);
  } else {
    throw new Error("Invalid argument: options");
  }
  
  // 二、执行编译
  if (callback) {
    if (typeof callback !== "function") {
      throw new Error("Invalid argument: callback");
    }
    
    // 监听模式
    if (
      options.watch === true ||
      (Array.isArray(options) && options.some(o => o.watch))
    ) {
      const watchOptions = Array.isArray(options)
        ? options.map(o => o.watchOptions || {})
        : options.watchOptions || {};
      return compiler.watch(watchOptions, callback);
    }
    
    // 单次编译
    compiler.run(callback);
  }
  return compiler;
};
事件Hook 类型描述
初始化参数-从配置文件和 Shell 语句中读取与合并参数,执行配置文件中的插件实例化语句 new Plugin()
实例化 Compiler-实例化 Compiler(负责文件监听和启动编译),包含完整的 webpack 配置,全局只有一个 Compiler 实例
加载插件-依次调用配置插件的 apply 方法,让插件可以监听后续的所有事件节点
environment
afterEnvironment
SyncHook应用 Node.js 风格的文件系统到 compiler 对象,方便后续文件查找和读取
entryOptionSyncBailHook读取配置的 entry,可在此阶段修改配置
afterPluginsSyncHook调用完所有内置和配置插件的 apply 方法后触发
afterResolversSyncHookresolver 设置完成后触发(resolver 负责在文件系统中寻找指定路径的文件)
initialize (v5)SyncHook编译器对象初始化完毕触发

2. 编译阶段

编译时有两种模式:

  • 监听模式(watch: true):监控文件系统变化,文件修改后触发完整重新编译
  • 单次编译模式(默认):执行一次完整编译
事件Hook 类型描述
beforeRunAsyncSeriesHook在开始执行一次构建之前调用(compiler.run 方法开始执行后立即调用)
runAsyncSeriesHook在开始读取 records(构建记录)之前调用,启动一次编译
watchRunAsyncSeriesHook监听模式下,重新编译触发后(但在 compilation 实际开始前)执行
normalModuleFactory
contextModuleFactory
SyncHook创建对应的工厂函数后执行。normalModuleFactory 负责创建普通模块(JS、CSS、图片等),contextModuleFactory 处理动态引入的模块(如 require.context)
beforeCompileAsyncSeriesHook创建 compilation parameter 后执行,可用于添加/修改 compilationParams
compileSyncHookbeforeCompile 之后立即调用(新的编译启动前)
thisCompilationSyncHook初始化 compilation 时调用(在触发 compilation 事件之前)
compilationSyncHook创建和管理模块构建过程的核心方法,主要功能:
1. 初始化 Compilation 实例(包含 modules/chunks/assets 等属性)
2. 提供构建流程的 hooks 系统
3. 协调核心操作:
- 模块依赖图构建
- Loader 执行与源码转换
- 模块依赖解析
- 优化处理(如 tree shaking)
- Chunks 生成
- 输出资源准备

注意:compilation hook 本身不直接执行构建,而是通过注册的插件和内部方法驱动上述流程
makeAsyncParallelHookcompilation 对象创建完毕,compilation 结束前执行(实际构建过程在此 hook 内完成)
afterCompileAsyncSeriesHookcompilation 结束后执行

3. 输出阶段

事件Hook 类型描述
shouldEmitSyncBailHook在输出 asset 之前调用。返回一个布尔值,告知是否输出。如果返回 false ,则不会生成任何输出文件到 output.path 目录,但compilation.assets 对象仍会保留所有资源,只是跳过了写入磁盘的步骤。如 webpack-dev-server
emitAsyncSeriesHook输出 asset 到 output 目录前执行,可修改输出内容
assetEmittedAsyncSeriesHook每个 asset 写入磁盘后立即触发,可访问输出路径和字节内容等信息
afterEmitAsyncSeriesHook输出 asset 到 output 目录后执行,可生成额外分析报告
doneAsyncSeriesHook编译完成

Compilation 流程及对应 Hooks

模块构建执行时机

模块构建执行时机是在 make 阶段。webpack 在初始化阶段通过 new EntryOptionPlugin().apply(compiler) 加载 EntryOptionPlugin 插件(执行时机在 entryOption 阶段)。该插件遍历 entry 对象,为每个入口加载 SingleEntryPlugin 或 MultiEntryPlugin:

const itemToPlugin = (context, item, name) => {
  if (Array.isArray(item)) {
    return new MultiEntryPlugin(context, item, name);
  }
  return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugin {
  apply(compiler) {
    compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
      if (typeof entry === "string" || Array.isArray(entry)) {
        itemToPlugin(context, entry, "main").apply(compiler);
      } else if (typeof entry === "object") {
        for (const name of Object.keys(entry)) {
          itemToPlugin(context, entry[name], name).apply(compiler);
        }
      } else if (typeof entry === "function") {
        new DynamicEntryPlugin(context, entry).apply(compiler);
      }
      return true;
    });
  }
};

以 SingleEntryPlugin 为例:

class SingleEntryPlugin {
  constructor(context, entry, name) {
    this.context = context;
    this.entry = entry;
    this.name = name;
  }

  // ...省略部分代码
  compiler.hooks.make.tapAsync(
    "SingleEntryPlugin",
    (compilation, callback) => {
      const { entry, name, context } = this;
      const dep = SingleEntryPlugin.createDependency(entry, name);
      compilation.addEntry(context, dep, name, callback);
    }
  );
}

在 make 阶段(已完成 compilation 构建)注册回调函数,通过执行 compilation.addEntry 启动构建流程。addEntry 方法通过 _addModuleChain 添加模块链并递归处理子依赖形成依赖图,然后通过 buildModule 执行完整构建流程(包括 Loader 执行和依赖解析)。

Compilation 常见 Hooks

事件Hook 类型描述
buildModuleSyncHook模块构建开始前触发,可修改模块源代码
normalModuleLoaderSyncHook普通模块 Loader,加载模块图中所有模块的函数
sealSyncHook编译封存阶段触发,禁止再修改/新增模块依赖关系
optimizeModulesSyncBailHook模块优化阶段开始时调用,可对模块进行优化
afterOptimizeModulesSyncHook模块优化完成后触发,用于分析优化结果或生成报告
optimizeChunksSyncBailHookChunk 优化阶段触发,用于合并或拆分 chunk
afterOptimizeChunksSyncHookChunk 优化完成后触发,常用于最终资源清单生成
optimizeTreeAsyncSeriesHook依赖树优化前触发,用于自定义依赖分析逻辑
additionalAssetsAsyncSeriesHook添加额外资源文件(如 favicon)
optimizeChunkAssetsAsyncSeriesHook资源优化阶段触发,可用于代码压缩
afterOptimizeAssetsSyncHook资源优化完成后触发,用于计算最终文件哈希

流程总结

  1. 参数初始化阶段:从配置文件(默认webpack.config.js)读取与合并参数,得出最终的参数
  2. 实例化 Compiler:用参数初始化 Compiler 对象,加载所有配置的插件
  3. 编译启动阶段:根据 entry 找出所有入口文件,实例化对应的 Compilation 对象(创建和管理模块构建过程的核心方法)
  4. 模块编译阶段(make 阶段):从入口文件出发,调用 Loader 翻译模块,并递归处理所有依赖模块。整个过程compilation各生命周期钩子被调用。
  5. Chunk 生成阶段:根据模块依赖关系将模块分组为 chunks(资源优化的关键步骤)
  6. Asset 生成阶段:通过模板引擎将 chunks 转换为最终 assets(存储在 compilation.assets 中)
  7. 文件输出阶段:根据配置确定输出路径和文件名,将内容写入文件系统