跳到正文
前端知识库
工程化

Webpack 扩展与构建生命周期

从 Compiler、Compilation、Module、Chunk、Bundle 到 Loader/Plugin、分包、压缩和可验证的自定义扩展。

6 分钟Webpack · Compiler · Compilation · Module · Chunk · Bundle · Loader · Plugin · HMR

Webpack 扩展与构建生命周期

1. 五个核心对象

对象 关注点
Compiler 一次完整构建的全局编排器,持有配置和生命周期 hooks
Compilation 某次构建的模块图、依赖、chunk、assets 和中间状态
Module 一个被解析/转换的源模块或资源模块
Chunk 按入口、动态导入和分包规则组织的一组模块
Bundle/Asset 最终写入磁盘的 JS、CSS、图片、字体等文件

可以把 Webpack 概括为:从 entry 出发解析依赖图,使用 loader 将模块转换成可分析内容,再由 plugin 参与生命周期,最后组装 chunk 和 assets 输出。

2. 构建主线

读取配置
  -> 创建 Compiler
  -> run/watch
  -> 根据 entry addEntry
  -> 解析依赖并创建 Module
  -> loader 转换、递归构建 moduleGraph
  -> seal:优化模块/Chunk、Tree Shaking、SplitChunks
  -> 生成 assets
  -> emit 写入文件系统

2.1 初始化

Webpack 读取 modeentryoutputmodule.rulesresolveexternals、plugins 等配置,创建 Compiler。插件通常在 apply(compiler) 中注册全局 hooks。

2.2 构建模块图

从入口模块开始,解析 import/require/动态导入;匹配 loader 并按规则转换源码,再解析转换结果中的依赖,直到模块图闭合。解析失败、loader 输出非法或依赖循环处理不当都会在这一阶段暴露。

2.3 生成和输出

模块图完成后,Webpack 根据入口、异步边界和 splitChunks 组装 chunk/chunk group,再生成 assets。生成阶段是修改输出内容的最后窗口之一;之后通过文件系统写入并触发完成 hooks。生产环境应保留构建 stats、release 和 source map 关联。

3. 常用 hooks 与选择

Hook 时机 典型用途
compiler.hooks.compilation 创建 Compilation 后 订阅本次模块图/资源生命周期
compiler.hooks.make 正式构建入口时 添加入口或异步构建任务
compilation.hooks.optimizeChunks chunk 集合形成后 自定义 chunk 优化或统计
compiler.hooks.emit 产物即将写出(旧式 API) 读取/修改 assets;新项目优先 processAssets
compilation.hooks.processAssets 资源处理阶段 按指定 stage 生成/修改 assets
compiler.hooks.done 构建完成 输出 stats、记录构建指标

Webpack 版本会调整 hooks 和 asset API。面试中可讲清时机和数据边界,实际代码应查目标版本类型声明,避免照抄旧版 compilation.assets[name] = ...

4. Loader:模块级转换函数

Loader 接收资源内容,返回转换后的内容或通过 callback 异步返回:

// replace-author-loader.cjs
const { getOptions } = require('loader-utils')

module.exports = function replaceAuthor(source, inputSourceMap) {
  const callback = this.async()
  const options = getOptions(this) || {}
  const author = options.author ?? 'anonymous'
  const output = source.replaceAll('{{author}}', author)

  // 替换可能改变字符长度,原 map 的列位置不再可靠;应生成新 map。
  // 这里选择明确丢弃旧 map,生产 loader 应使用 source-map 工具重建映射。
  callback(null, output, null)
}

Loader 需要考虑:

  • this.resourcePaththis.getOptions()this.addDependency() 等上下文;
  • 同步返回还是 this.async() callback;
  • loader 链从右到左/从下到上的执行顺序(以目标版本文档为准);
  • 输入类型(字符串/Buffer)、source map、错误位置和缓存标识;
  • 只处理 include 范围内的文件,避免扫描 node_modules 或生成目录;
  • 配置可序列化,确保持久化缓存可命中。

Loader 只负责“一个模块如何变换”,不应偷偷修改整个构建或写任意目录。涉及全局资源、入口和输出清单时,应考虑 Plugin。

5. Plugin:流程级扩展对象

Plugin 通常是带 apply 方法的对象:

class BuildNoticePlugin {
  constructor(options = {}) {
    this.filename = options.filename ?? 'build-notice.txt'
  }

  apply(compiler) {
    compiler.hooks.thisCompilation.tap('BuildNoticePlugin', (compilation) => {
      const { RawSource } = compiler.webpack.sources
      compilation.hooks.processAssets.tap(
        {
          name: 'BuildNoticePlugin',
          stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
        },
        () => {
          const names = Object.keys(compilation.assets).sort()
          compilation.emitAsset(
            this.filename,
            new RawSource(`Generated assets:\n${names.join('\n')}\n`),
          )
        },
      )
    })
  }
}

module.exports = BuildNoticePlugin

这个插件展示了“需求 -> hook -> 输入/输出 -> 错误处理 -> 测试”的最小闭环。实际项目还应处理重复生成、资产命名冲突、source map、watch 模式和构建取消。

6. Loader 与 Plugin 的区别

维度 Loader Plugin
抽象 函数 对象/类的 apply
范围 单个模块或资源 Compiler/Compilation 全流程
输入输出 source -> source/AST/asset hooks、module graph、chunks、assets
例子 TS、JSX、Sass、Markdown 转换 HTML、清单、压缩、分包、分析、HMR
典型测试 输入 fixture 到输出 fixture hook 时机、stats 和 assets

简单记忆:Loader 改“文件内容”,Plugin 改“构建过程”。一个功能可能同时需要二者,但边界要明确。

7. 分包、Tree Shaking 和资源优化

7.1 splitChunks

optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendors: {
        test: /[\\/]node_modules[\\/]/,
        priority: -10,
      },
    },
  },
  runtimeChunk: 'single',
}

按路由动态导入、公共依赖、更新频率和缓存收益切分;不要只追求 chunk 数量或把所有第三方打进一个永不更新的大包。过细分包会增加请求、解析和调度成本,需结合 Network 和 RUM 验证。

7.2 Tree Shaking

Tree Shaking 依赖 ESM 的静态结构、生产模式、side effects 声明和代码写法。CommonJS 动态导出、错误的 sideEffects: false、模块顶层注册和聚合入口都可能削弱或破坏摇树。CSS、polyfill 和全局注册应明确列为副作用。

7.3 CSS 提取与压缩

MiniCssExtractPlugin 等工具可将 CSS 从 JS 中提取成独立文件,便于并行下载和缓存;按路由拆分时要处理 FOUC、关键 CSS 和样式顺序。css-minimizer-webpack-plugin/cssnano 负责压缩,但删除未使用规则前要覆盖动态 class、主题和 SSR。

7.4 Terser 与 source map

Terser 可压缩、折叠和删除不可达代码;是否删除 console 应由日志策略决定。生产 source map 不应公开源码,上传到受控错误平台,并以 release/构建 hash 确保映射一致。

7.5 Dll 与现代替代

DllPlugin 通过预构建稳定第三方依赖减少重复构建,在历史 Webpack 工程中可能有价值;Webpack 5 filesystem cache、Vite 依赖预构建和稳定 lockfile 通常是更易维护的优先方案。选择要以 profile 和团队维护成本为证据,不要因为名词“高级”就引入。

8. 构建速度诊断

  1. 记录冷启动、增量构建、CI 构建和产物压缩分别耗时;
  2. 查看 loader/plugin timing、profile 和缓存命中率;
  3. 收窄 include/exclude,启用 filesystem cache,减少无关插件;
  4. 仅在 CPU/内存足够且 profile 证明有效时并行 transform/压缩;
  5. 检查 source map、依赖预构建和 Monorepo 软链包是否造成额外处理;
  6. 用相同 Node、锁文件、机器和配置复测,并把结果纳入 CI 趋势。

构建快不等于产物好:类型检查、Lint、测试、source map、可重复性和错误定位仍是交付质量的一部分。

9. HMR 链路

文件变更 -> watcher -> 增量编译 -> 生成 hash/模块更新
  -> dev server/WebSocket 推送 -> 客户端接受并替换 -> 状态保留或 full reload

若模块没有可接受边界、状态不可恢复或更新冒泡到入口,HMR 会退化为整页刷新。排查时同时看终端编译日志、浏览器 HMR 消息、模块边界和插件转换结果。React/Vue 的 Fast Refresh/SFC HMR 由框架插件提供额外规则,并非 Webpack 单独保证。

10. 自定义扩展测试

Loader

  • 输入多种编码、空文件和异常语法;
  • 验证同步/异步 callback 只调用一次;
  • 检查 source map、选项和缓存依赖;
  • 确认不处理 include 外文件。

Plugin

  • 用最小 fixture 运行真实 Webpack 编译;
  • 断言 hook 时机、生成 assets、文件内容和命名冲突;
  • 测试 watch/重复编译/错误中止;
  • 检查升级 Webpack 后类型和 API 变化。

11. 面试追问速答

Q: Webpack 的 Module、Chunk、Bundle 怎么区分?

A: Module 是源码或资源模块;Chunk 是按入口/异步边界/分包规则组织的一组模块;Bundle/Asset 是最终写到磁盘的文件。一个 chunk 可能生成一个或多个 asset。

Q: Loader 为什么不能替代 Plugin?

A: Loader 作用于单个模块输入输出,Plugin 通过 compiler/compilation hooks 参与全局流程,才能访问入口、chunk、资产和构建统计。把流程级逻辑塞进 loader 会破坏缓存和职责边界。

Q: 自定义 Plugin 怎么选择 hook?

A: 先明确需求发生在初始化、模块构建、chunk 优化、asset 处理还是完成统计,再选择对应 compiler/compilation hook,并查目标 Webpack 版本的 stage 和 API。不要只凭 emit 旧示例。

Q: 如何证明优化有效?

A: 用固定环境的 profile 和产物报告对比构建耗时、入口/路由传输字节、解析执行、缓存命中率,再用真实页面的 LCP/INP/错误率验证;如果只改善本地启动,不应宣称线上收益。

12. 资料来源与现有专题

本篇整理 前端架构师工程化思维与编译原理详解.pdf 的 Compiler/Compilation/Module/Chunk/Bundle、初始化/构建/生成阶段、hooks、自定义 Loader/Plugin、SplitChunks、Tree Shaking、Dll、CSS 提取和 Terser 内容,并采用 Webpack 5 的 asset API 做校正。相关专题:Webpack 构建流程Loader 与 Plugin构建体积与性能治理