Webpack 打包流程及 Hooks 汇总

工作原理概述

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

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

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

Webpack 打包流程详解

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

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

核心概念

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

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

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

Plugin Hooks

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

  1. 同步 Hooks(如 compilation):

    • 使用 .tap() 注册的回调会在 hook 触发时立即执行

    • 执行顺序遵循注册顺序

    • 类型:

      • SyncHook:顺序执行所有注册的回调函数,忽略返回值
      • SyncBailHook:顺序执行所有注册的回调函数,当回调函数返回值不为undefined时,立即停止后续回调函数的执行,如 entryOption。这样当某个插件已经确定了最终配置时,可以跳过剩余插件的执行
      js 复制代码
      // 多个插件处理 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。
js 复制代码
// 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. 初始化阶段

js 复制代码
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 对象,方便后续文件查找和读取
entryOption SyncBailHook 读取配置的 entry,可在此阶段修改配置
afterPlugins SyncHook 调用完所有内置和配置插件的 apply 方法后触发
afterResolvers SyncHook resolver 设置完成后触发(resolver 负责在文件系统中寻找指定路径的文件)
initialize (v5) SyncHook 编译器对象初始化完毕触发

2. 编译阶段

编译时有两种模式:

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

3. 输出阶段

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

Compilation 流程及对应 Hooks

模块构建执行时机

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

js 复制代码
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 为例:

js 复制代码
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 类型 描述
buildModule SyncHook 模块构建开始前触发,可修改模块源代码
normalModuleLoader SyncHook 普通模块 Loader,加载模块图中所有模块的函数
seal SyncHook 编译封存阶段触发,禁止再修改/新增模块依赖关系
optimizeModules SyncBailHook 模块优化阶段开始时调用,可对模块进行优化
afterOptimizeModules SyncHook 模块优化完成后触发,用于分析优化结果或生成报告
optimizeChunks SyncBailHook Chunk 优化阶段触发,用于合并或拆分 chunk
afterOptimizeChunks SyncHook Chunk 优化完成后触发,常用于最终资源清单生成
optimizeTree AsyncSeriesHook 依赖树优化前触发,用于自定义依赖分析逻辑
additionalAssets AsyncSeriesHook 添加额外资源文件(如 favicon)
optimizeChunkAssets AsyncSeriesHook 资源优化阶段触发,可用于代码压缩
afterOptimizeAssets SyncHook 资源优化完成后触发,用于计算最终文件哈希

流程总结

  1. 参数初始化阶段:从配置文件(默认webpack.config.js)读取与合并参数,得出最终的参数
  2. 实例化 Compiler:用参数初始化 Compiler 对象,加载所有配置的插件
  3. 编译启动阶段:根据 entry 找出所有入口文件,实例化对应的 Compilation 对象(创建和管理模块构建过程的核心方法)
  4. 模块编译阶段 (make 阶段):从入口文件出发,调用 Loader 翻译模块,并递归处理所有依赖模块。整个过程compilation各生命周期钩子被调用。
  5. Chunk 生成阶段:根据模块依赖关系将模块分组为 chunks(资源优化的关键步骤)
  6. Asset 生成阶段:通过模板引擎将 chunks 转换为最终 assets(存储在 compilation.assets 中)
  7. 文件输出阶段:根据配置确定输出路径和文件名,将内容写入文件系统
相关推荐
小高0073 分钟前
📈前端图片压缩实战:体积直降 80%,LCP 提升 2 倍
前端·javascript·面试
OEC小胖胖7 分钟前
【React Hooks】封装的艺术:如何编写高质量的 React 自-定义 Hooks
前端·react.js·前端框架·web
BillKu15 分钟前
vue3+element-plus 输入框el-input设置背景颜色和字体颜色,样式效果等同于不可编辑的效果
前端·javascript·vue.js
惊悚的毛毛虫19 分钟前
掘金免广告?不想看理财交流圈?不想看exp+8?
前端
springfe010125 分钟前
vue3组件 - 大文件上传
前端·vue.js
再学一点就睡33 分钟前
Vite 工作原理(简易版)—— 从代码看核心逻辑
前端·vite
好好好明天会更好1 小时前
uniapp项目中小程序的生命周期
前端·vue.js
CF14年老兵1 小时前
「Vue 3 + View Transition 实现炫酷圆形缩放换肤动画」
前端·css·trae
小璞1 小时前
05_CursorRules_代码审查篇_Rule_code-review
前端