本篇是 Rspack 源码解析系列第十二篇。第十篇讲到
CreateChunkAssetsPass会通过render_manifestHook 产出最终 asset。本文沿rspack_plugin_javascript继续往下看:一个 JavaScript Chunk 如何从模块生成结果、runtime module 和 chunk graph,变成最终的main.js或异步 chunk 文件。
前言:Core 不直接拼 JavaScript
在前面的主线里,Core 负责构建 ModuleGraph、ChunkGraph、RuntimeRequirements、Hash 和 Asset 生命周期。但真正到了输出 JavaScript 文件时,Core 并不会把模块表、runtime、启动代码写死在自己内部。
它做的是调用:
text
compilation_hooks.render_manifest
然后由 JavaScript 插件向 manifest 中追加条目。
这点非常关键:
text
Core 负责阶段和数据边界
JavaScript Plugin 负责 JS 输出格式
也就是说,main.js、lazy.js、hot update chunk 并不是 Core 的固定产物,而是 JS 插件基于 Chunk 信息渲染出来的文件。
源码入口:CreateChunkAssetsPass 只负责调度
这一篇如果只讲 render_manifest,很容易停留在"插件产出 asset"的概念层。先看真实入口:
text
crates/rspack_core/src/compilation/create_chunk_assets/mod.rs
CreateChunkAssetsPass 的 run_pass 很短:
rust
async fn run_pass(&self, compilation: &mut Compilation) -> Result<()> {
let plugin_driver = compilation.plugin_driver.clone();
compilation.create_chunk_assets(plugin_driver).await?;
Ok(())
}
这段源码说明:Core 的这个 Pass 并不拼 JavaScript,也不理解 CSS、Wasm 的输出格式。它只是进入 chunk asset 阶段,然后把控制权交给 Compilation::create_chunk_assets。
继续看 create_chunk_assets,真正调用插件的位置是:
rust
plugin_driver
.compilation_hooks
.render_manifest
.call(this, chunk, &mut manifests, &mut diagnostics)
.await?;
这里的 manifests 是一个 Vec<RenderManifestEntry>。也就是说,Core 对每个 Chunk 调一次 render_manifest,具体生成 JS、CSS 还是 Wasm,由 tap 到这个 Hook 上的插件决定。
源码里还通过 rspack_futures::scope 并发处理多个 Chunk。这个细节很重要:到这里时,模块图、ChunkGraph、runtime requirements、hash 都应该已经稳定,asset 阶段只是基于稳定中间结果做渲染。
RenderManifestEntry 可以理解成插件交给 Core 的"出货单":
rust
pub struct RenderManifestEntry {
pub source: BoxSource,
pub filename: String,
pub has_filename: bool,
pub info: AssetInfo,
pub auxiliary: bool,
}
随后 Core 统一执行:
rust
self.emit_asset(
filename.clone(),
CompilationAsset::new(Some(file_manifest.source), file_manifest.info),
);
所以 Compilation.assets 不是 JS 插件随手写进去的,而是 Core 根据 manifest entry 统一入库。
JsPlugin 不只是最后拼文件
rspack_plugin_javascript 参与的阶段很多,不只是 asset 渲染。它会注册 JavaScript 相关的 parser、generator、dependency template、runtime requirement、hash 和 render manifest。
可以把它理解成一条贯穿编译过程的 JS 语义插件:
text
解析 JS 模块
-> 识别 import / require / import()
-> 生成 Dependency
-> 代码生成时替换依赖表达式
-> 收集 runtime requirements
-> 计算 JS content hash
-> 渲染 JS chunk asset
这也是为什么 JavaScript 插件在 Rspack 中非常核心。它并不是"最后把字符串拼起来"的工具,而是把 JS 语言语义接入整个编译流水线。
源码看 JsPlugin 如何接入 asset 渲染
JavaScript 插件注册 Hook 的源码在:
text
crates/rspack_plugin_javascript/src/plugin/impl_plugin_for_js_plugin.rs
impl Plugin for JsPlugin 里可以看到:
rust
ctx.compiler_hooks.compilation.tap(compilation::new(self));
ctx.compilation_hooks.additional_tree_runtime_requirements.tap(...);
ctx.compilation_hooks.chunk_hash.tap(chunk_hash::new(self));
ctx.compilation_hooks.content_hash.tap(content_hash::new(self));
ctx.compilation_hooks.render_manifest.tap(render_manifest::new(self));
这几行把 JS 插件的源码角色串起来了:
text
compilation:注册 JS dependency factory / template
additional_tree_runtime_requirements:补充 JS runtime 能力
chunk_hash / content_hash:参与 JS 内容 hash
render_manifest:最终生成 JS asset
所以 render_manifest 不是孤立存在的最后一步。它依赖前面 Hook 已经把 JS 语义、依赖模板、runtime、hash 都接入了 Compilation。
render_manifest:先判断这个 Chunk 是否需要 JS 文件
一个 Chunk 不一定要产出 JavaScript 文件。比如某些 Chunk 可能只包含 CSS,或者只包含辅助资源。
因此 JS 插件在 render_manifest 阶段首先要判断:
text
这个 Chunk 是否包含 JavaScript 模块?
这个 Chunk 是否包含 runtime module?
它是不是 runtime chunk?
它是不是 hot update chunk?
只有满足条件时,才会向 manifest 中追加 JS asset。
源码中这部分判断就在 render_manifest 函数开头:
rust
let is_hot_update = matches!(chunk.kind(), ChunkKind::HotUpdate);
let is_main_chunk = chunk.groups().iter().any(|group_ukey| { ... });
let is_runtime_chunk =
chunk.has_runtime(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey);
if !is_hot_update
&& is_runtime_chunk
&& !chunk_has_runtime_or_js(...)
{
return Ok(());
}
if !is_hot_update && !is_main_chunk && !is_runtime_chunk && !chunk_has_js(chunk_ukey, compilation) {
return Ok(());
}
chunk_has_js 又会继续检查 SourceType::JavaScript:
rust
chunk_graph.has_chunk_module_by_source_type(
chunk_ukey,
SourceType::JavaScript,
compilation.get_module_graph(),
)
这段源码把原理落到了数据结构上:Chunk 只是容器,是否输出 JS,要看这个 Chunk 里是否真的存在 JavaScript source type 的模块,或者是否携带 JS runtime module。
这一点说明 Chunk 是一个更高层的容器,它可以包含多种 source type:
text
Chunk
├─ JavaScript modules
├─ CSS modules
├─ Runtime modules
├─ Wasm modules
└─ auxiliary assets
最终是否生成 .js,由 JavaScript 插件决定;是否生成 .css,则由 CSS 插件决定。
文件名不是简单字符串拼接
确认要生成 JavaScript 后,插件会根据 output 配置和 Chunk 信息得到文件名模板,再把模板展开成真实文件名。
典型配置是:
js
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[id].[contenthash].js'
}
这里会用到:
text
chunk name
chunk id
runtime
content hash
full hash
output public path
所以文件名生成依赖前面多个阶段的稳定结果。没有第八篇的 ID 分配,没有第十篇的 hash 计算,[contenthash] 就没有办法落地。
对应源码是:
rust
let filename_template = get_js_chunk_filename_template(
chunk,
&compilation.options.output,
&compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
);
let output_path = compilation
.get_path_with_info(
&filename_template,
PathData::default()
.chunk_hash_optional(chunk.rendered_hash(...))
.chunk_id_optional(chunk.id().map(|id| id.as_str()))
.chunk_name_optional(chunk.name_for_filename_template())
.content_hash_optional(chunk.rendered_content_hash_by_source_type(
&compilation.chunk_hashes_artifact,
&SourceType::JavaScript,
compilation.options.output.hash_digest_length,
))
.runtime(chunk.runtime().as_str()),
&mut asset_info,
)
.await?;
这里能清楚看到 [id]、[name]、[chunkhash]、[contenthash]、[runtime] 分别来自 Chunk、hash artifact 和 runtime 信息。
render_main 与 render_chunk
JS 插件渲染时通常会区分入口/runtime Chunk 和普通异步 Chunk。
可以简化成:
text
入口或 runtime chunk
-> render_main
普通异步 chunk
-> render_chunk
hot update chunk
-> hot update render
源码中的分流逻辑也在 render_manifest 里:
rust
let source = if let Some(source) = hooks.render_chunk_content.call(...).await? {
source.source
} else if is_hot_update {
self.render_chunk(...).await?
} else if is_runtime_chunk {
self.render_main(...).await?
} else {
self.render_chunk(...).await?
};
也就是说,入口/runtime Chunk 与普通异步 Chunk 在源码层面就走向不同函数:runtime chunk 走 render_main,普通 chunk 走 render_chunk。
入口 Chunk 通常承担启动应用的职责:
text
安装模块表
-> 安装 runtime modules
-> 初始化 __webpack_require__
-> 执行 entry module
异步 Chunk 则更像一个"模块安装包":
text
告诉 runtime:
这里有一组新的模块
它们属于某个 chunk id
加载完成后可以继续执行 import() 后续逻辑
所以 main.js 和 lazy.js 的渲染模型并不相同。入口文件要启动,异步文件主要负责注册。
模块代码在这里已经不是原始源码
到 render chunk 时,模块的 source 已经经历过代码生成。
例如源码:
js
import { add } from './math';
console.log(add(1, 2));
在模块 code generation 阶段,import dependency 会通过对应 template 改写成运行时可执行的形式。最终 render chunk 拿到的是模块生成结果,而不是原始文本。
换句话说:
text
Parser / Dependency / Generator
解决单个模块内部怎么变
Chunk Renderer
解决多个模块如何组织成文件
这两个层次不能混在一起。
import() 的完整链路
用动态导入再串一下:
js
import('./lazy').then(mod => mod.run());
它会经历:
text
Parser
-> 识别 DynamicImport Dependency
BuildModuleGraph
-> lazy module 进入模块图
BuildChunkGraph
-> lazy module 被放入异步 Chunk
CodeGeneration
-> import() 被改写成 runtime 调用
RuntimeRequirements
-> 添加 ensure_chunk、chunk loading 等能力
CreateHash
-> 计算 main / lazy 的 content hash
render_manifest
-> main 走 render_main
-> lazy 走 render_chunk
Compilation.assets
-> main.[hash].js
-> lazy.[hash].js
这条线把前面多篇文章串了起来:模块图决定有什么,ChunkGraph 决定怎么分组,runtime requirements 决定需要哪些运行时代码,render manifest 决定最终文件。
Chunk render cache
Rspack 会尽量复用稳定 Chunk 的渲染结果。若模块生成结果、runtime module、hash 和输出信息都没有变化,就没有必要重新渲染整个 JS 文件。
这和前面反复提到的增量构建模式一致:
text
有 Artifact
-> 根据 Mutation 找 affected chunks
-> 清理失效结果
-> 复用其余结果
JavaScript 渲染虽然是最终阶段,但仍然受 Artifact 和 Mutation 体系管理。
Rust 角度:插件拥有格式,Core 拥有生命周期
这部分源码最值得关注的不是字符串拼接细节,而是边界划分:
text
Core:
管 Compilation、Graph、Hook、Artifact、Cache
JavaScript Plugin:
管 JS 语义、JS runtime、JS chunk 输出格式
这样做的好处是,Core 不需要知道所有资源类型的渲染细节。CSS、Wasm、Asset 都可以通过类似的插件方式接入统一生命周期。
这一篇应该带走什么
- JavaScript 文件由
rspack_plugin_javascript通过render_manifest生成; - Core 不直接拼 JS bundle,而是提供 Chunk、CodeGenerationResult、Runtime 等稳定输入;
- 入口/runtime Chunk 和异步 Chunk 的渲染职责不同;
- render chunk 拿到的模块 source 已经经过 dependency template 和 code generation;
- JS 渲染阶段同样受 Artifact、Mutation 和缓存边界管理。
写在最后
JavaScript 插件代表 Rust 内部插件的一条线;另一条更复杂的线是 JS 生态兼容。Webpack 插件运行在 JavaScript 世界里,而 Rspack Core 运行在 Rust 世界里。下一篇我们沿 rspack_binding_api 看看 JS Hook 是如何接入 Rust Core 的。