Rspack 源码解析(十六):多类型资源如何进入 Compilation.assets

本篇是 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,也能继续扩展新的资源类型。

这一篇应该带走什么

  1. Chunk 可以包含多种 source type,不只有 JavaScript;
  2. render_manifest 是多插件共同填充的产物协议;
  3. Module assets、Chunk assets、Process assets 对应不同产物时机;
  4. CSS、Asset、Wasm 等资源通过各自插件进入 Compilation.assets;
  5. Rspack 的关键不是统一所有资源实现,而是统一资源生命周期和产物抽象。

写在最后

到第十六篇,我们已经从 Rspack 的构建主线走到了插件、解析、loader 和多资源输出。继续深入可以按两条线展开:一条是从具体插件入手,例如 CSS、Asset、Html、SplitChunks;另一条是从性能入手,继续研究 incremental、cache、并发调度和 tracing。前者帮助理解功能实现,后者帮助理解 Rspack 为什么快。

相关推荐
小小善后师1 小时前
用 Canvas + AI 实现登录页的 Logo 粒子动画
前端·vue.js
GAMC1 小时前
chrome-devtools-mcp:让 AI 编码助手真正"看见"浏览器
前端·人工智能
deli0071 小时前
高尔顿板:把球一颗颗丢下去,为什么最后总堆成一座钟形山
前端
Norna1 小时前
时间轮里那行凭空出现的 +1
rust
java_nnnn1 小时前
JavaEE进阶-CSS初识
java·前端·css·java-ee·html
good_ideal1 小时前
从零实现一个自动提交 PR 的 MCP 工具:用 Skill 串起 Git 与 Azure DevOps
前端
喜欢吃豆1 小时前
Agent 前端协议正在分层:彻底讲清 MCP Apps、AG-UI 与 A2UI
前端·大模型
赵锦川1 小时前
css代替表格
前端·css
其实防守也摸鱼1 小时前
内网安全防护实践:从攻击视角理解防御要点
java·前端·安全·架构·自动化·nps