Rspack 源码解析(十一):资产 Hook 与增量构建

文章来源声明: 原文作者:Amos_zz; 来源站点:掘金; 原文链接:https://juejin.cn/post/7687580061679763506; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

适合已了解 Rspack 构建主线的开发者,帮助理解插件钩子设计与增量缓存边界,对设计高扩展、可缓存的构建系统有直接参考价值。

Rspack 源码解析(十一):资产 Hook 与增量构建 -----------------------------

本篇是 Rspack 源码解析系列第十一篇,也是这条构建主线的收束篇。ProcessAssetsPassAfterProcessAssetsPassAfterSealPass 位于 asset 生成之后;它们把最终处理权交给插件。本文同时回看贯穿整条流水线的 Cache、Mutation 和 Artifact,建立对 Rspack 增量构建设计的整体认识。

前言:生成文件不是终点

第十篇结束时,Compilation.assets 中已有 JS、CSS 以及模块附属资源。但压缩、生成 HTML、生成 manifest、更新 source map 等工作通常必须看到完整资产集合。因此流水线最后三步是:

CreateChunkAssetsPass
  -> ProcessAssetsPass
  -> AfterProcessAssetsPass
  -> AfterSealPass

它们的实现短,原因不是工作少,而是所有具体能力都被放进插件 Hook。

ProcessAssets:面向完整 asset 集合的扩展点

ProcessAssetsPass 的主体只有:

<span>async</span> <span>fn</span> <span>process_assets</span>(&<span>mut</span> <span>self</span>, plugin_driver: SharedPluginDriver) <span>-></span> <span>Result</span><()> {
  plugin_driver.compilation_hooks.process_assets
    .<span>call</span>(<span>self</span>)
    .<span>await</span>
    .<span>map_err</span>(|e| e.<span>wrap_err</span>(<span>"caused by plugins in Compilation.hooks.processAssets"</span>))
}

这里的 self 是完整的 Compilation,插件可以读取、更新、追加或删除 assets。典型能力包括:

插件行为输入输出
压缩 JS/CSS已生成 source更小的 asset source
Html 插件entry 文件、public path新的 `index.html`
manifest 插件所有 asset 名称和 hash映射 JSON 文件
source map 处理source 与 map 元数据额外 `.map` 文件或内联信息

之所以在此阶段做,而不是在 Module::code_generation() 做,是因为这些操作的单位是最终文件,可能需要同时看到多个 Chunk。

AfterProcessAssets 与 AfterSeal:生命周期的最后两个同步点

后两个 Pass 的语义分别是:

after_process_assets:所有 processAssets 插件完成后执行
after_seal:整个 seal / compilation 阶段结束前的最终回调

它们让插件不必通过“猜测其他插件执行顺序”来协作:需要修改 asset 时挂在 process_assets,只观察最终结果或做收尾时挂在 after Hook。

这也是插件化系统最重要的价值之一:阶段顺序成为公开契约,插件之间不直接耦合实现细节。

PluginDriver:从 trait 到 Hook

每个 Rust 插件实现 Plugin trait,并在 apply 时向 ApplyContext 注册 Hook。PluginDriver 保存多个 hook 集合:

CompilerHooks
CompilationHooks
NormalModuleFactoryHooks
ContextModuleFactoryHooks
NormalModuleHooks
ConcatenatedModuleHooks

编译 Pass 不需要枚举“有哪些插件”,它只调用像 compilation_hooks.process_assets 这样的 typed Hook。对比将插件保存为 Vec<Box<dyn Plugin>> 后在每个阶段手写遍历,这种设计有三个好处:

  1. Hook 参数在编译期固定,插件不能拿错阶段数据;
  2. Series、SeriesBail 等 Hook 明确表达顺序与短路/重跑语义;
  3. 每个 Pass 只暴露它允许修改的 artifact,减少跨阶段误修改。

Cache、Artifact、Mutation:增量构建的三件套

前面各篇不断出现三个概念,它们组成 watch 模式能快的基础:

概念定位例子
`Artifact`某阶段可复用的计算结果ChunkGraph、CodeGenerationResults、Chunk hash
`Mutation`本轮相对上一轮的变化记录`ModuleUpdate`、`ChunkRemove`、`ModuleSetHashes`
`Cache`在 Pass 前后参与恢复和持久化的统一接口`before_module_ids`、`after_chunk_asset`

一次文件变更后的通用模式:

watch 检测到 src/a.ts 改动
  -> BuildModuleGraph 记录 ModuleUpdate
  -> 后续 Pass 读取 Mutation
  -> 计算 affected modules / chunks
  -> 清理这些对象的旧 Artifact
  -> 复用其余 Artifact
  -> 只重新生成必要的代码与 assets

例如 CreateChunkAssetsPass 会读取 Mutation::ChunkSetHashes,只渲染 hash 改变的 Chunk;而输出文件名依赖 [fullhash] 时,它会关闭局部缓存路径,因为一个全局 hash 变化可能改变所有文件名。

为什么每个 Pass 都有 before / after cache hook

部分 Pass 实现:

<span>async</span> <span>fn</span> <span>before_pass</span>(&<span>self</span>, compilation: &<span>mut</span> Compilation, cache: &<span>mut</span> <span>dyn</span> Cache) {
  cache.<span>before_chunk_asset</span>(compilation).<span>await</span>;
}

<span>async</span> <span>fn</span> <span>after_pass</span>(&<span>self</span>, compilation: &<span>mut</span> Compilation, cache: &<span>mut</span> <span>dyn</span> Cache) {
  cache.<span>after_chunk_asset</span>(compilation).<span>await</span>;
}

PassExt 把这类模板行为放进统一调度中。具体 Pass 只描述自己的业务操作,缓存系统在确定的边界恢复或记录状态。这个设计将“编译正确性”和“缓存加速”分层:禁用缓存不会改变构建语义,只会回退为更多全量计算。

全系列主线回顾

CLI / JS API
  -> N-API binding
  -> Compiler 创建 Compilation
  -> Build Module Graph
  -> Finish / Seal / Dependency Optimization
  -> Build Chunk Graph
  -> Graph Optimization
  -> Module / Chunk / Runtime IDs
  -> Code Generation
  -> Runtime Requirements
  -> Hash + Runtime Module Code Generation
  -> Module Assets + Chunk Assets
  -> Process Assets + After Hooks
  -> 输出 dist

从 Rust 源码学习的角度,整条线反复出现同一种架构:

显式 Artifact 作为阶段输入输出
  + typed Hook 作为扩展边界
  + 所有权迁移控制可变状态
  + 可并行计算与集中写回分离
  + Mutation 驱动局部重算

这比把所有状态塞进一个 Compilation 可变对象、在任意位置修改更适合大型编译器:依赖可见、缓存边界可见、并发边界也可见。

这一篇应该带走什么

  1. process_assets 是面向最终文件集合的核心插件阶段;
  2. after Hook 提供可靠的收尾时序,避免插件之间猜测执行顺序;
  3. PluginDriver + typed Hook 是 Rspack Core 可扩展且类型安全的关键;
  4. Artifact 保存结果、Mutation 描述变化、Cache 管理跨 Pass 复用;
  5. 增量构建的本质是精确失效与局部重算,不是简单重复执行全流程。

写在最后

至此,围绕 Compilation::run_passes() 的核心构建流水线已经完整走通。继续深入可以选择两条支线:沿 rspack_plugin_javascript 追一个 render_manifest 如何把模块 source 渲染成 bundle,或沿 rspack_binding_api 和 JS Hook 追一次跨 N-API 的插件调用。前者适合继续研究生成细节,后者适合理解 Rspack 如何兼容 webpack 生态。