本篇是 Rspack 源码解析系列第二十四篇。第十六篇从多 source type 角度扫过 asset module,但没进入实现。本文进入
rspack_plugin_asset,看一个.png如何既在 JS 里变成一个 URL 字符串,又在Compilation.assets里成为真实文件------同一个模块的双重身份。
前言:asset 的四种形态
Asset module 常见四种处理方式:
js
// asset:根据大小自动 inline 或 resource
import url from './logo.png';
// asset/inline:强制 data URL
import url from './icon.svg';
// asset/resource:强制输出文件
import url from './font.woff2';
// asset/source:导出原始字符串
import src from './text.txt';
四种形态对应 AssetParserAndGenerator 的四个构造函数。
源码入口:CanonicalizedDataUrlOption
crates/rspack_plugin_asset/src/lib.rs 先把用户配置规范化:
rust
#[cacheable]
#[derive(Debug, Clone)]
pub enum CanonicalizedDataUrlOption {
Source,
Bytes,
Asset(IsInline),
}
其中 Asset(IsInline) 的 IsInline 是 bool:
rust
const ASSET_INLINE: bool = true;
const ASSET_RESOURCE: bool = false;
所以最终形态被压缩成三种:
text
Source:导出原始字符串(asset/source)
Bytes:导出原始字节(asset/bytes)
Asset(inline):inline 成 data URL
Asset(resource):输出文件
四个构造函数
rust
impl AssetParserAndGenerator {
pub fn with_auto(option: Option<AssetParserDataUrl>, emit: bool) -> Self { ... }
pub fn with_inline() -> Self { ... }
pub fn with_resource(emit: bool) -> Self { ... }
pub fn with_source() -> Self { ... }
pub fn with_bytes() -> Self { ... }
}
在 apply 里,五个 ModuleType 分别注册不同的构造函数:
rust
ctx.register_parser_and_generator_builder(
rspack_core::ModuleType::Asset,
Box::new(move |parser_options, generator_options| {
let data_url_condition = parser_options.and_then(|x| x.get_asset()).and_then(|x| x.data_url_condition.clone());
let emit: Option<bool> = generator_options.and_then(|x| x.get_asset()).and_then(|x| x.emit);
Box::new(AssetParserAndGenerator::with_auto(data_url_condition, emit.unwrap_or(true)))
}),
);
Asset 用 with_auto(按 dataUrlCondition.maxSize 自动判断),AssetInline/AssetResource/AssetSource/AssetBytes 分别用固定构造函数。
AssetExportsDependency:default 导出
asset_exports_dependency.rs 很短,核心是声明一个 default 导出:
rust
impl Dependency for AssetExportsDependency {
fn get_exports(&self, ...) -> Option<ExportsSpec> {
Some(ExportsSpec {
exports: ExportsOfExportsSpec::Names(vec![ExportNameOrSpec::String("default".into())]),
priority: Some(1),
terminal_binding: Some(true),
..Default::default()
})
}
fn could_affect_referencing_module(&self) -> rspack_core::AffectType {
rspack_core::AffectType::False
}
}
terminal_binding: Some(true) 表示「这个导出就是最终绑定,不要再往下解析」。AffectType::False 表示 asset 模块不会影响引用它的模块(没有副作用传播)。
source_types 的按需判断
Asset 模块的 source_types 不是固定的,而是根据「谁在引用我」来决定:
rust
fn source_types(&self, module: &dyn Module, module_graph: &ModuleGraph) -> &[SourceType] {
let mut source_types = HashSet::new();
let module_id = module.identifier();
for connection in module_graph.get_incoming_connections(&module_id) {
if let Some(module) = connection.original_module_identifier.and_then(|id| module_graph.module_by_identifier(&id)) {
let module_type = module.module_type();
source_types.insert(SourceType::from(module_type));
} else {
let dependency = module_graph.dependency_by_id(&connection.dependency_id);
if matches!(dependency.dependency_type(), DependencyType::LoaderImport) {
source_types.insert(SourceType::JavaScript);
}
}
}
// ...
}
如果被 JS 模块 import,就需要 JavaScript source type(生成 URL 字符串);被 CSS 的 url() 引用,就需要 CssUrl source type。一个 asset 可能同时被 JS 和 CSS 引用,所以 source_types 是动态集合。
hash_for_source:contenthash 计算
rust
fn hash_for_source(&self, source: &BoxSource, compiler_options: &CompilerOptions) -> RspackHashDigest {
let mut hasher = RspackHash::from(&compiler_options.output);
hasher.write(&source.buffer());
hasher.digest(&compiler_options.output.hash_digest)
}
asset 的 contenthash 就是「文件字节的 hash」,用于生成 logo.[contenthash].png 这样的文件名。这也是长期缓存的基础:文件内容不变,文件名不变。
generate:三种 source type 三种输出
generate 里按 requested_source_type 分支:
SourceType::Asset(输出真实文件)
rust
SourceType::Asset => {
if parsed_asset_config.is_source() || parsed_asset_config.is_inline() {
Err(error!("Inline or Source asset does not have source type `asset`"))
} else {
let contenthash = self.hash_for_source(source, &compilation.options);
let contenthash = contenthash.rendered(compilation.options.output.hash_digest_length);
// ...
let (_, filename, mut asset_info) = self
.get_asset_module_filename(normal_module, module_generator_options, compilation, Some(contenthash), &source_file_name, true)
.await?;
// 通过 CodeGenerationDataFilename 记录文件名
// ...
Ok(source.clone().boxed())
}
}
CodeGenerationDataFilename 会被后续 render_manifest 读取,从而把真实文件写进 Compilation.assets。
SourceType::JavaScript / CssUrl(生成 URL 字符串)
rust
// resource 形态
let asset_path = if import_mode.is_preserve() {
// 保留 import 语句
serde_json::to_string(&format!("{AUTO_PUBLIC_PATH_PLACEHOLDER}{original_filename}"))...
} else if let Some(public_path) = module_generator_options.and_then(|x| x.asset_public_path()) {
// 自定义 publicPath
...
} else {
format!(
r#"{} + "{}""#,
generate_context.runtime_template.render_runtime_globals(&RuntimeGlobals::PUBLIC_PATH),
original_filename
)
};
最终生成的 JS 大致是:
js
module.exports = __webpack_public_path__ + "logo.0fd23ab1.png";
inline:data URL 的生成
data URL 需要 mime type 和编码。get_mimetype 用 mime_guess 根据文件扩展名推断:
rust
if let Some(resource_path) = resource_data.path() {
return mime_guess::MimeGuess::from_path(resource_path)
.first_raw()
.map(ToOwned::to_owned)
.ok_or_else(|| {
error!("DataUrl can't be generated automatically, because there is no mimetype for \"{ext:?}\" ...")
});
}
get_encoding 和 get_encoded_content 处理 base64 编码:
rust
if encoding == DEFAULT_ENCODING {
return Ok(base64::encode_to_string(source.buffer()));
}
render_manifest:把 asset 文件写进 chunk
Asset 插件也实现了 CompilationRenderManifest:
rust
#[plugin_hook(CompilationRenderManifest for AssetPlugin)]
async fn render_manifest(
&self,
compilation: &Compilation,
chunk_ukey: &ChunkUkey,
manifest: &mut Vec<RenderManifestEntry>,
_diagnostics: &mut Vec<Diagnostic>,
) -> Result<()> {
// 找到 SourceType::Asset 的模块
let ordered_modules = compilation.build_chunk_graph_artifact.chunk_graph
.get_chunk_modules_identifier_by_source_type(chunk_ukey, SourceType::Asset, module_graph);
let assets = ordered_modules
.par_iter()
.map(|mid| {
let code_gen_result = compilation.code_generation_results.get(mid, Some(chunk.runtime()));
let result = code_gen_result.get(&SourceType::Asset).map(|source| {
let asset_filename = code_gen_result.data.get::<CodeGenerationDataFilename>()...;
let asset_info = code_gen_result.data.get::<CodeGenerationDataAssetInfo>()...;
RenderManifestEntry {
source: source.clone(),
filename: asset_filename.to_owned(),
has_filename: true,
info: asset_info.with_asset_type(ManifestAssetType::Asset),
auxiliary: true, // asset 是辅助文件
}
});
Ok(result)
})
.collect::<Result<Vec<Option<RenderManifestEntry>>>>()?
.into_par_iter()
.flatten()
.collect::<Vec<RenderManifestEntry>>();
manifest.extend(assets);
Ok(())
}
注意 auxiliary: true:asset 文件是 chunk 的辅助文件,不是 chunk 主体(JS/CSS bundle)。这和第 10、16 篇讲的 render_manifest 协议完全一致。
AUTO_PUBLIC_PATH_PLACEHOLDER
源码里有个占位符:
rust
pub const AUTO_PUBLIC_PATH_PLACEHOLDER: &str = "__RSPACK_PLUGIN_ASSET_AUTO_PUBLIC_PATH__";
当 publicPath: 'auto' 时,JS 里先写占位符,渲染阶段再根据 chunk 实际位置替换成相对路径。这和 CSS 插件里的 AUTO_PUBLIC_PATH_PLACEHOLDER 是同一个思路:publicPath 的最终值要在 render 阶段才知道。
Rust 角度:一个模块,两种输出
Asset 插件最核心的设计是「按 source type 分离输出」:
text
同一个 AssetParserAndGenerator
-> SourceType::Asset 生成真实文件
-> SourceType::JavaScript 生成 URL 字符串
-> SourceType::CssUrl 生成 CSS url()
模块还是那一个模块,但不同消费方看到不同的「生成结果」。这是 CodeGenerationResult 按 SourceType 分 key 的价值所在------第 5 篇就埋下了这个伏笔。
这一篇应该带走什么
- asset 有 source/bytes/inline/resource 四种形态,压缩成三种
CanonicalizedDataUrlOption; source_types根据「谁引用我」动态决定,一个 asset 可同时服务 JS 和 CSS;SourceType::Asset输出真实文件,JavaScript/CssUrl输出 URL 字符串;- contenthash 是文件字节 hash,是长期缓存基础;
- render_manifest 把 asset 作为 chunk 的 auxiliary file 写入。
写在最后
asset 是「纯值模块」的极致:连 exports_depth 都没有,只有 default 导出。接下来换一个完全不同的方向------CSS。它有自己的词法分析、依赖类型、顺序冲突检测,还有独立于 JS 的 content hash。下一篇进入 rspack_plugin_css,看 CSS 如何成为一等公民模块。