Rspack 源码解析(十):Hash 与 Asset 生成

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

适合想深入 Rspack 构建链路与增量缓存的开发者,清晰拆解 hash 粒度、辅助文件与 manifest 渲染边界,能直接指导插件开发和产物优化。

> 本篇是 Rspack 源码解析系列第十篇。经过 runtime requirements 阶段后,Rspack 已经拥有生成 asset 所需的全部语义信息。本篇追踪 `CreateHashPass`、`CreateModuleAssetsPass` 和 `CreateChunkAssetsPass`,理解 `CodeGenerationResults` 如何成为带 hash 的 `main.js`、异步 chunk 和附属资源。

前言:源码变成文件之前还有两层

第五篇的结果是每个 Module 在某个 runtime 下的 source;第九篇补上 runtime modules。距离 dist/main.js 仍需两步:

中间结果
  -> 计算 module / chunk / compilation hash
  -> 由插件提供 render manifest
  -> emit_asset 写入 Compilation.assets

源码中的执行顺序为:

CreateHashPass
  -> CreateModuleAssetsPass
  -> CreateChunkAssetsPass

CreateHashPass:hash 不是一个值

Pass 入口做两件事:

<span>async</span> <span>fn</span> <span>run_pass</span>(&<span>self</span>, compilation: &<span>mut</span> Compilation) <span>-></span> <span>Result</span><()> {
  compilation.<span>create_hash</span>(plugin_driver).<span>await</span>?;
  compilation.<span>runtime_modules_code_generation</span>().<span>await</span>?;
  <span>Ok</span>(())
}

第一步计算 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。这个模式和前文相同:

没有可用 artifact:遍历全部 Chunk
有可用 artifact + Mutation:只计算 affected chunks
发现 full hash 全局依赖:禁用局部缓存路径

这里不能简单地“每个 Chunk 各算各的”:异步加载文件名、runtime manifest、library 格式等都可能把 Chunk 联系起来。Rspack 明确检测这种关系,而不是错误复用局部结果。

CreateModuleAssets:处理模块自带的附属文件

CreateModuleAssetsPass 扫描所有模块的 build_info().assets

<span>for</span> (identifier, module) <span>in</span> mg.<span>modules</span>() {
  <span>let</span> <span></span><span>assets</span> = &module.<span>build_info</span>().assets;
  <span>for</span> (name, asset) <span>in</span> assets.<span>as_ref</span>() {
    module_assets.<span>push</span>((name.<span>clone</span>(), asset.<span>clone</span>()));
  }
  <span>for</span> <span>chunk</span> <span>in</span> chunk_graph.<span>get_module_chunks</span>(*identifier) {
    chunk_asset_map.<span>push</span>((chunk, name.<span>clone</span>()));
  }
}

典型来源是 asset module、loader 或插件在构建模块时产出的图片、字体、source map 等。Pass 做两件事:

  1. 调用 emit_asset(name, asset) 把文件放进 compilation 的 asset 集合;
  2. 若模块属于某个 Chunk,则在 Chunk 上标记该文件为 auxiliary file。

辅助文件不会作为 JS Chunk 的主体内容,却必须在 stats、清理旧文件和发布产物时与 Chunk 建立关联。

CreateChunkAssets:用 render manifest 产出 bundle

核心调用不是直接遍历 CodeGenerationResults 拼字符串,而是:

plugin_driver.compilation_hooks.render_manifest
  .<span>call</span>(compilation, chunk, &<span>mut</span> manifests, &<span>mut</span> diagnostics)
  .<span>await</span>?;

每个 FileManifest 包含:

filename
source
asset info
auxiliary 标记

内置的 JavaScript、CSS、source map 等插件各自向 manifest 添加条目。这一点解释了为什么 Rspack 可以支持不同输出格式和 target:Core 负责“对每个 Chunk 请求一份渲染清单”,插件负责“如何渲染这个 Chunk”。

生成结果随后写回:

current_chunk.<span>set_rendered</span>(<span>true</span>);
current_chunk.<span>add_file</span>(filename.<span>clone</span>());
compilation.<span>emit_asset</span>(filename, CompilationAsset::<span>new</span>(<span>Some</span>(source), info));

这时 Compilation.assets 才出现真正的 main.jslazy.js 等文件。

为什么 Chunk 资产可以并发生成

源码使用 rspack_futures::scope 为多个 Chunk 建立任务。前提是图、ID、code generation result 和 hash 都已稳定;每个任务只为一个 Chunk 收集 manifest,最终在主流程集中写回 artifact 和 assets。

稳定的 Compilation 视图
  -> 多个 chunk render_manifest 并发运行
  -> 收集 (ChunkUkey, ChunkRenderResult)
  -> 顺序 emit_asset / 更新 Chunk 元数据

这与第三篇的“后台构建、主队列改图”是同一原则:可并发的纯计算并行;共享状态写入保持可控。

import() 再走一遍

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

这一篇应该带走什么

  1. hash 有 Module、Chunk content、Full compilation 等不同粒度;
  2. full hash 依赖会破坏局部增量计算,Rspack 会显式降级;
  3. Module assets 是模块构建副产物,通常作为 Chunk 的 auxiliary file;
  4. Chunk 的实际渲染由 render_manifest Hook 驱动,而不是由 Core 固定拼接;
  5. 图稳定后,Chunk 级渲染可以并行,最终 asset 写入统一收束。

写在最后

到这里文件已经进入 Compilation.assets,但还不是编译真正结束。插件仍可在 process_assets 中压缩、生成 manifest、修改 source map 或注入额外文件。下一篇会讲整个收尾 Hook 链,以及它与缓存和 watch 增量编译的关系。