本篇是 Rspack 源码解析系列第十六篇。第十二篇详细看了 JavaScript Chunk 的渲染,但真实项目的输出远不止 JS。CSS、图片、字体、Wasm、SourceMap、HTML 都可能成为最终资源。本文从多 source type 的角度,理解 Rspack 如何让不同插件共同产出
Compilation.assets。
前言:现代构建产物不是一个 bundle
一个普通 Web 项目的输出可能是:
text
dist/
main.8f3a2c1e.js
vendors.7a91de03.js
main.4ac812aa.css
logo.0fd23ab1.png
icon.woff2
async.wasm
index.html
main.js.map
这些文件不是同一个插件生成的。
| 文件 | 可能来源 |
|---|---|
| JS | JavaScript 插件 |
| CSS | CSS / Extract CSS 插件 |
| 图片、字体 | asset module 或 loader |
| Wasm | Wasm 插件 |
| HTML | Html 插件 |
| SourceMap | devtool 相关插件 |
Rspack 需要用统一的生命周期管理这些不同资源。
源码入口:render_manifest 不只是 JS 插件
在当前项目中搜索 render_manifest,可以看到多个插件都实现了这个 Hook,例如:
text
crates/rspack_plugin_javascript/src/plugin/impl_plugin_for_js_plugin.rs
crates/rspack_plugin_css/src/plugin/impl_plugin_for_css_plugin.rs
crates/rspack_plugin_extract_css/src/plugin.rs
crates/rspack_plugin_asset/src/lib.rs
crates/rspack_plugin_wasm/src/wasm_plugin.rs
这说明 render_manifest 是 Chunk 级资源输出协议,不是 JavaScript 专用协议。
Core 侧最终统一处理 manifest entry 的源码在:
text
crates/rspack_core/src/compilation/create_chunk_assets/mod.rs
rust
for file_manifest in manifests {
if file_manifest.auxiliary {
current_chunk.add_auxiliary_file(filename.clone());
} else {
current_chunk.add_file(filename.clone());
}
self.emit_asset(
filename.clone(),
CompilationAsset::new(Some(file_manifest.source), file_manifest.info),
);
}
所以多类型资源的统一点不是"所有插件共用一套生成逻辑",而是"所有插件最终都交出 RenderManifestEntry"。
Chunk 可以包含多种 source type
前面讲 JavaScript 渲染时,我们一直关注 SourceType::JavaScript。但 Chunk 并不只属于 JS。
可以把 Chunk 理解成一组模块和运行时关系的容器:
text
Chunk
├─ JavaScript source type
├─ CSS source type
├─ Wasm source type
├─ runtime modules
└─ auxiliary assets
不同插件只关心自己负责的 source type。
例如:
text
JavaScript 插件
-> 生成 main.js / lazy.js
CSS 插件
-> 生成 main.css
Wasm 插件
-> 生成 xxx.wasm,并补充加载逻辑
这解释了为什么 content hash 也要按 source type 区分。同一个 Chunk 的 JS 内容变化,不一定意味着 CSS 内容变化。
JS 插件源码里就明确标记了资源类型:
rust
let mut asset_info = AssetInfo::default().with_asset_type(ManifestAssetType::JavaScript);
asset_info.set_javascript_module(compilation.options.output.module);
最后 push:
rust
manifest.push(RenderManifestEntry {
source,
filename: output_path,
has_filename: false,
info: asset_info,
auxiliary: false,
});
这意味着后续 stats、HTML 插件或其他 process assets 插件可以通过 AssetInfo 知道这个 asset 是 JavaScript,以及它是否是 ESM 输出。
render_manifest 是统一产物协议
第十篇讲过,CreateChunkAssetsPass 会对每个 Chunk 调用 render_manifest。
重点是:这个 Hook 上不只有 JavaScript 插件。
多个插件都可以向 manifest 中追加条目:
text
同一个 Chunk
|
|-- JS 插件追加 main.js
|-- CSS 插件追加 main.css
|-- Wasm 插件追加 async.wasm
|-- source map 插件追加 main.js.map
Core 最终拿到的是一组 manifest entry,然后统一写入 Compilation.assets。
这就是插件和 Core 之间的边界:
text
插件负责生产 source
Core 负责统一入库
RenderManifestEntry 像一张出货单
一个 manifest entry 至少表达:
text
文件名是什么
内容 source 是什么
asset info 是什么
是否属于 auxiliary file
Core 不需要知道 CSS 是怎么拼的,也不需要知道 Wasm 二进制如何生成。它只要根据 manifest 把文件加入资产集合,并更新 Chunk 的 files / auxiliary files。
AssetInfo 里还包含资源类型:
rust
pub enum ManifestAssetType {
Unknown,
Asset,
Css,
JavaScript,
Wasm,
Custom(Ustr),
}
所以 Compilation.assets 更接近:
text
filename -> { source, info: { asset_type, content_hash, related, immutable, ... } }
而不是简单的 filename -> string。
这和前面的设计一脉相承:
text
复杂实现留给插件
统一生命周期留给 Core
Module assets 与 Chunk assets
资源进入 Compilation.assets 的路径不止一种。
第十篇提到过 CreateModuleAssetsPass,它处理模块构建阶段产生的资产:
text
module.build_info().assets
典型来源包括:
text
asset module
loader emitFile
某些模块级插件
而 CreateChunkAssetsPass 处理的是 Chunk 级别资产:
text
main.js
lazy.js
main.css
可以对比:
| 类型 | 产出时机 | 例子 |
|---|---|---|
| Module asset | 模块构建时 | 图片、字体、loader emitFile |
| Chunk asset | Chunk 渲染时 | JS bundle、CSS bundle、Wasm chunk |
| Process asset | 完整 asset 集合之后 | HTML、manifest、压缩产物 |
最终它们都会汇入 Compilation.assets。
Asset module 的两种形态
图片、字体等资源常见两种处理方式:
text
inline:
变成 data URL,嵌入 JS 或 CSS
resource:
作为单独文件输出
例如:
js
import logo from './logo.png';
可能生成:
js
export default __webpack_public_path__ + 'logo.0fd23ab1.png';
同时 logo.0fd23ab1.png 会作为 asset 写入 compilation。
这类模块有双重身份。
从源码角度看,asset module 的 JS 表达和文件输出是两条线:
text
parser / generator 生成 JS 中可使用的 URL 表达式
render_manifest 或 module asset 阶段输出真实文件
因此 import logo from './logo.png' 最终既可能在 JS 模块中变成一个 URL 字符串,也可能在 Compilation.assets 中多出一个 logo.[hash].png。
这类模块有双重身份:
text
对 JS 来说,它导出一个 URL
对输出系统来说,它是一个真实文件
CSS 为什么适合独立插件
CSS 和 JS 的输出模型不同。
JS 关心:
text
模块函数包装
__webpack_require__
runtime modules
startup
chunk loading
CSS 关心:
text
选择器顺序
@import
url()
CSS modules
提取成文件还是注入 JS
source map
因此 CSS 不适合作为 JavaScript 渲染器内部的一段附属逻辑。它更适合作为独立 source type,由 CSS 插件在 render manifest 阶段产出自己的 asset。
这也说明现代 bundler 里 CSS 不是简单字符串,而是进入了模块图、Chunk 和 asset 生命周期。
Wasm:既是模块,也是资源
Wasm 比图片更特殊。它既是一个二进制资源,又可能被 JS runtime 加载和实例化。
所以 Wasm 插件通常要处理两件事:
text
1. 输出 .wasm 文件
2. 让 JS runtime 知道如何加载它
Wasm 插件也走同一套 manifest 协议。它会在当前 Chunk 的模块里找 ModuleType::WasmAsync,再从 code generation 结果里取 SourceType::Wasm,最后构造 RenderManifestEntry。
这一条链路可以概括为:
text
ChunkGraph 找到 WasmAsync 模块
-> CodeGenerationResults 取 SourceType::Wasm
-> 根据 webassembly_module_filename 生成文件名
-> AssetInfo 标记 ManifestAssetType::Wasm
-> push RenderManifestEntry
它会同时影响:
text
ModuleGraph
ChunkGraph
RuntimeRequirements
Compilation.assets
这说明多资源类型不是简单"复制文件",而是会参与完整编译语义。
HTML 和 manifest 更适合 process_assets
HTML 插件通常需要看到完整资产列表:
text
入口 JS
入口 CSS
publicPath
hash
async chunks
asset info
因此它不适合在某个单独 Chunk 的 render manifest 阶段生成,而更适合在 process_assets 阶段处理。
例如生成:
html
<script src="/main.8f3a2c1e.js"></script>
<link rel="stylesheet" href="/main.4ac812aa.css">
它必须等 JS、CSS 等资源都进入 Compilation.assets 后,才能得到完整视图。
这对应第十一篇的结论:
text
先生成 Chunk assets
再在 process_assets 基于完整集合做二次处理
AssetInfo:文件内容之外的元数据
每个 asset 除了 source,还会有 asset info。
它可能包含:
text
content hash
是否 immutable
是否 minimized
source map 关系
development 标记
hot update 标记
这些信息会影响:
text
stats 输出
缓存策略
HTML 注入
清理旧资源
插件后处理
所以 Compilation.assets 不是简单的:
text
HashMap<String, String>
而更接近:
text
filename -> { source, info }
多插件写 assets 如何避免混乱
如果所有插件都能随便写文件,最容易出现:
text
文件名冲突
重复 emit
执行顺序不确定
asset 被后续插件意外覆盖
Rspack 通过阶段化 Hook 降低混乱:
text
CreateModuleAssetsPass
-> CreateChunkAssetsPass
-> ProcessAssetsPass
-> AfterProcessAssetsPass
-> AfterSealPass
不同操作放在不同阶段:
| 阶段 | 适合做什么 |
|---|---|
| module assets | 模块构建副产物 |
| chunk assets | JS/CSS/Wasm bundle |
| process assets | 压缩、HTML、manifest、source map |
| after hooks | 观察最终结果或收尾 |
这让插件之间不必靠猜测顺序协作。
一个页面的资源汇总
假设入口代码是:
js
import './style.css';
import logo from './logo.png';
console.log(logo);
import('./lazy');
可能产生:
text
JavaScript:
main.[hash].js
lazy.[hash].js
CSS:
main.[hash].css
Asset:
logo.[hash].png
Process assets:
index.html
manifest.json
最终统一汇入:
text
Compilation.assets
这就是现代 bundler 的核心输出模型:多个来源、多种资源、统一资产集合。
Rust 角度:统一抽象比统一实现更重要
Rspack 没有强行让所有资源都走同一个生成函数。它做的是定义统一边界:
text
Module
CodeGenerationResult
SourceType
RenderManifestEntry
CompilationAsset
AssetInfo
然后让不同插件在边界内实现自己的逻辑。
这种架构适合大型构建器:
text
统一生命周期
+ 多插件实现细节
+ 标准化产物入口
既能支持 JS/CSS/Wasm/Asset,也能继续扩展新的资源类型。
这一篇应该带走什么
- Chunk 可以包含多种 source type,不只有 JavaScript;
render_manifest是多插件共同填充的产物协议;- Module assets、Chunk assets、Process assets 对应不同产物时机;
- CSS、Asset、Wasm 等资源通过各自插件进入
Compilation.assets; - Rspack 的关键不是统一所有资源实现,而是统一资源生命周期和产物抽象。
写在最后
到第十六篇,我们已经从 Rspack 的构建主线走到了插件、解析、loader 和多资源输出。继续深入可以按两条线展开:一条是从具体插件入手,例如 CSS、Asset、Html、SplitChunks;另一条是从性能入手,继续研究 incremental、cache、并发调度和 tracing。前者帮助理解功能实现,后者帮助理解 Rspack 为什么快。