本篇是 Rspack 源码解析系列第十篇。经过 runtime requirements 阶段后,Rspack 已经拥有生成 asset 所需的全部语义信息。本篇追踪
CreateHashPass、CreateModuleAssetsPass和CreateChunkAssetsPass,理解CodeGenerationResults如何成为带 hash 的main.js、异步 chunk 和附属资源。
前言:源码变成文件之前还有两层
第五篇的结果是每个 Module 在某个 runtime 下的 source;第九篇补上 runtime modules。距离 dist/main.js 仍需两步:
text
中间结果
-> 计算 module / chunk / compilation hash
-> 由插件提供 render manifest
-> emit_asset 写入 Compilation.assets
源码中的执行顺序为:
text
CreateHashPass
-> CreateModuleAssetsPass
-> CreateChunkAssetsPass
CreateHashPass:hash 不是一个值
Pass 入口做两件事:
rust
async fn run_pass(&self, compilation: &mut Compilation) -> Result<()> {
compilation.create_hash(plugin_driver).await?;
compilation.runtime_modules_code_generation().await?;
Ok(())
}
第一步计算 hash,第二步为第九篇已选择的 RuntimeModule 生成 source。之所以 runtime module 的生成放在 hash 后面,是因为它的内容和名字也必须参与正确的缓存失效关系。
至少要区分三类 hash:
| 名称 | 代表什么 | 常见用途 |
|---|---|---|
| Module hash | 单个模块生成结果的内容/语义 | 复用 code generation cache |
| Chunk content hash | 一个 Chunk 中特定 source type 的内容 | [contenthash] 文件名 |
| Full hash | 整次 compilation 的整体标识 | [fullhash] 与全局关联内容 |
如果某个 Chunk 依赖 full hash,局部变化可能影响全局命名。源码会通过 dependent_full_hash Hook 找到这类 Chunk,并禁用相关增量 Pass,因为它已经不是局部可重算的问题。
Hash 的并行与增量边界
create_hash() 会并行检查 Chunk 是否依赖 full hash,并根据 Mutation 选取受影响 Chunk。这个模式和前文相同:
text
没有可用 artifact:遍历全部 Chunk
有可用 artifact + Mutation:只计算 affected chunks
发现 full hash 全局依赖:禁用局部缓存路径
这里不能简单地"每个 Chunk 各算各的":异步加载文件名、runtime manifest、library 格式等都可能把 Chunk 联系起来。Rspack 明确检测这种关系,而不是错误复用局部结果。
CreateModuleAssets:处理模块自带的附属文件
CreateModuleAssetsPass 扫描所有模块的 build_info().assets:
rust
for (identifier, module) in mg.modules() {
let assets = &module.build_info().assets;
for (name, asset) in assets.as_ref() {
module_assets.push((name.clone(), asset.clone()));
}
for chunk in chunk_graph.get_module_chunks(*identifier) {
chunk_asset_map.push((chunk, name.clone()));
}
}
典型来源是 asset module、loader 或插件在构建模块时产出的图片、字体、source map 等。Pass 做两件事:
- 调用
emit_asset(name, asset)把文件放进 compilation 的 asset 集合; - 若模块属于某个 Chunk,则在 Chunk 上标记该文件为 auxiliary file。
辅助文件不会作为 JS Chunk 的主体内容,却必须在 stats、清理旧文件和发布产物时与 Chunk 建立关联。
CreateChunkAssets:用 render manifest 产出 bundle
核心调用不是直接遍历 CodeGenerationResults 拼字符串,而是:
rust
plugin_driver.compilation_hooks.render_manifest
.call(compilation, chunk, &mut manifests, &mut diagnostics)
.await?;
每个 FileManifest 包含:
text
filename
source
asset info
auxiliary 标记
内置的 JavaScript、CSS、source map 等插件各自向 manifest 添加条目。这一点解释了为什么 Rspack 可以支持不同输出格式和 target:Core 负责"对每个 Chunk 请求一份渲染清单",插件负责"如何渲染这个 Chunk"。
生成结果随后写回:
rust
current_chunk.set_rendered(true);
current_chunk.add_file(filename.clone());
compilation.emit_asset(filename, CompilationAsset::new(Some(source), info));
这时 Compilation.assets 才出现真正的 main.js、lazy.js 等文件。
为什么 Chunk 资产可以并发生成
源码使用 rspack_futures::scope 为多个 Chunk 建立任务。前提是图、ID、code generation result 和 hash 都已稳定;每个任务只为一个 Chunk 收集 manifest,最终在主流程集中写回 artifact 和 assets。
text
稳定的 Compilation 视图
-> 多个 chunk render_manifest 并发运行
-> 收集 (ChunkUkey, ChunkRenderResult)
-> 顺序 emit_asset / 更新 Chunk 元数据
这与第三篇的"后台构建、主队列改图"是同一原则:可并发的纯计算并行;共享状态写入保持可控。
用 import() 再走一遍
text
CodeGenerationResults
main: index.ts + math.ts
lazy: lazy.ts
|
v
Runtime requirements
main 需要加载 lazy 的 helper
|
v
CreateHash
main content hash、lazy content hash
|
v
render_manifest
main -> main.[hash].js
lazy -> lazy.[hash].js
|
v
Compilation.assets
这一篇应该带走什么
- hash 有 Module、Chunk content、Full compilation 等不同粒度;
- full hash 依赖会破坏局部增量计算,Rspack 会显式降级;
- Module assets 是模块构建副产物,通常作为 Chunk 的 auxiliary file;
- Chunk 的实际渲染由
render_manifestHook 驱动,而不是由 Core 固定拼接; - 图稳定后,Chunk 级渲染可以并行,最终 asset 写入统一收束。
写在最后
到这里文件已经进入 Compilation.assets,但还不是编译真正结束。插件仍可在 process_assets 中压缩、生成 manifest、修改 source map 或注入额外文件。下一篇会讲整个收尾 Hook 链,以及它与缓存和 watch 增量编译的关系。