Rspack 源码解析(十一):资产 Hook 与增量构建
本篇是 Rspack 源码解析系列第十一篇,也是这条构建主线的收束篇。
ProcessAssetsPass、AfterProcessAssetsPass、AfterSealPass位于 asset 生成之后;它们把最终处理权交给插件。本文同时回看贯穿整条流水线的 Cache、Mutation 和 Artifact,建立对 Rspack 增量构建设计的整体认识。
前言:生成文件不是终点
第十篇结束时,Compilation.assets 中已有 JS、CSS 以及模块附属资源。但压缩、生成 HTML、生成 manifest、更新 source map 等工作通常必须看到完整资产集合。因此流水线最后三步是:
text
CreateChunkAssetsPass
-> ProcessAssetsPass
-> AfterProcessAssetsPass
-> AfterSealPass
它们的实现短,原因不是工作少,而是所有具体能力都被放进插件 Hook。
ProcessAssets:面向完整 asset 集合的扩展点
ProcessAssetsPass 的主体只有:
rust
async fn process_assets(&mut self, plugin_driver: SharedPluginDriver) -> Result<()> {
plugin_driver.compilation_hooks.process_assets
.call(self)
.await
.map_err(|e| e.wrap_err("caused by plugins in Compilation.hooks.processAssets"))
}
这里的 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 的语义分别是:
text
after_process_assets:所有 processAssets 插件完成后执行
after_seal:整个 seal / compilation 阶段结束前的最终回调
它们让插件不必通过"猜测其他插件执行顺序"来协作:需要修改 asset 时挂在 process_assets,只观察最终结果或做收尾时挂在 after Hook。
这也是插件化系统最重要的价值之一:阶段顺序成为公开契约,插件之间不直接耦合实现细节。
PluginDriver:从 trait 到 Hook
每个 Rust 插件实现 Plugin trait,并在 apply 时向 ApplyContext 注册 Hook。PluginDriver 保存多个 hook 集合:
text
CompilerHooks
CompilationHooks
NormalModuleFactoryHooks
ContextModuleFactoryHooks
NormalModuleHooks
ConcatenatedModuleHooks
编译 Pass 不需要枚举"有哪些插件",它只调用像 compilation_hooks.process_assets 这样的 typed Hook。对比将插件保存为 Vec<Box<dyn Plugin>> 后在每个阶段手写遍历,这种设计有三个好处:
- Hook 参数在编译期固定,插件不能拿错阶段数据;
- Series、SeriesBail 等 Hook 明确表达顺序与短路/重跑语义;
- 每个 Pass 只暴露它允许修改的 artifact,减少跨阶段误修改。
Cache、Artifact、Mutation:增量构建的三件套
前面各篇不断出现三个概念,它们组成 watch 模式能快的基础:
| 概念 | 定位 | 例子 |
|---|---|---|
Artifact |
某阶段可复用的计算结果 | ChunkGraph、CodeGenerationResults、Chunk hash |
Mutation |
本轮相对上一轮的变化记录 | ModuleUpdate、ChunkRemove、ModuleSetHashes |
Cache |
在 Pass 前后参与恢复和持久化的统一接口 | before_module_ids、after_chunk_asset |
一次文件变更后的通用模式:
text
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 实现:
rust
async fn before_pass(&self, compilation: &mut Compilation, cache: &mut dyn Cache) {
cache.before_chunk_asset(compilation).await;
}
async fn after_pass(&self, compilation: &mut Compilation, cache: &mut dyn Cache) {
cache.after_chunk_asset(compilation).await;
}
PassExt 把这类模板行为放进统一调度中。具体 Pass 只描述自己的业务操作,缓存系统在确定的边界恢复或记录状态。这个设计将"编译正确性"和"缓存加速"分层:禁用缓存不会改变构建语义,只会回退为更多全量计算。
全系列主线回顾
text
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 源码学习的角度,整条线反复出现同一种架构:
text
显式 Artifact 作为阶段输入输出
+ typed Hook 作为扩展边界
+ 所有权迁移控制可变状态
+ 可并行计算与集中写回分离
+ Mutation 驱动局部重算
这比把所有状态塞进一个 Compilation 可变对象、在任意位置修改更适合大型编译器:依赖可见、缓存边界可见、并发边界也可见。
这一篇应该带走什么
process_assets是面向最终文件集合的核心插件阶段;- after Hook 提供可靠的收尾时序,避免插件之间猜测执行顺序;
PluginDriver + typed Hook是 Rspack Core 可扩展且类型安全的关键;- Artifact 保存结果、Mutation 描述变化、Cache 管理跨 Pass 复用;
- 增量构建的本质是精确失效与局部重算,不是简单重复执行全流程。
写在最后
至此,围绕 Compilation::run_passes() 的核心构建流水线已经完整走通。继续深入可以选择两条支线:沿 rspack_plugin_javascript 追一个 render_manifest 如何把模块 source 渲染成 bundle,或沿 rspack_binding_api 和 JS Hook 追一次跨 N-API 的插件调用。前者适合继续研究生成细节,后者适合理解 Rspack 如何兼容 webpack 生态。