前言:runtime 是「产物的操作系统」
bundle 跑起来需要一套支撑代码:
模块注册(__webpack_require__)
chunk 加载(动态 import 按需拉取)
公共路径(publicPath 解析)
启动(entry 的首次执行)
这套代码不是写死的,而是根据 target、chunkLoading、output.module 等配置「按需组装」。rspack_plugin_runtime 就是组装它的地方。
源码入口:enable_chunk_loading_plugin
crates/rspack_plugin_runtime/src/lib.rs 的 enable_chunk_loading_plugin 根据 ChunkLoadingType 选择插件:
pub fn enable_chunk_loading_plugin(loading_type: ChunkLoadingType, plugins: &mut Vec<BoxPlugin>) {
match loading_type {
ChunkLoadingType::Jsonp => {
plugins.push(StartupChunkDependenciesPlugin::new(
ChunkLoading::Enable(ChunkLoadingType::Jsonp), true).boxed());
plugins.push(JsonpChunkLoadingPlugin::default().boxed());
}
ChunkLoadingType::Require => {
plugins.push(StartupChunkDependenciesPlugin::new(
ChunkLoading::Enable(ChunkLoadingType::Require), false).boxed());
plugins.push(CommonJsChunkLoadingPlugin::new(false).boxed())
}
ChunkLoadingType::AsyncNode => {
plugins.push(StartupChunkDependenciesPlugin::new(
ChunkLoading::Enable(ChunkLoadingType::AsyncNode), true).boxed());
plugins.push(CommonJsChunkLoadingPlugin::new(true).boxed())
}
ChunkLoadingType::ImportScripts => {
plugins.push(StartupChunkDependenciesPlugin::new(
ChunkLoading::Enable(ChunkLoadingType::ImportScripts), true).boxed());
plugins.push(ImportScriptsChunkLoadingPlugin::default().boxed());
}
ChunkLoadingType::Import => {
plugins.push(StartupChunkDependenciesPlugin::new(
ChunkLoading::Enable(ChunkLoadingType::Import), true).boxed());
plugins.push(ModuleChunkLoadingPlugin::default().boxed())
}
ChunkLoadingType::Custom(_) => (),
}
}
五种加载方式对应五种插件:
| 类型 | 插件 | 场景 |
|---|---|---|
| Jsonp | JsonpChunkLoadingPlugin | 浏览器(script 标签) |
| Require | CommonJsChunkLoadingPlugin(false) | Node(同步 require) |
| AsyncNode | CommonJsChunkLoadingPlugin(true) | Node(异步 import) |
| ImportScripts | ImportScriptsChunkLoadingPlugin | Web Worker |
| Import | ModuleChunkLoadingPlugin | 原生 ESM |
两个核心 Hook
每个 chunk loading 插件都 tap 两个 Hook:
CompilationAdditionalTreeRuntimeRequirements:在 chunk 图里标记 runtime requirement;CompilationRuntimeRequirementInTree:根据 requirement 注入 runtime module。
以 JsonpChunkLoadingPlugin 为例(jsonp_chunk_loading.rs):
#[plugin_hook(CompilationAdditionalTreeRuntimeRequirements for JsonpChunkLoadingPlugin)]
async fn additional_tree_runtime_requirements(&self, compilation, chunk_ukey, runtime_requirements, ...) {
let is_enabled = is_enabled_for_chunk(chunk_ukey, &ChunkLoading::Enable(ChunkLoadingType::Jsonp), compilation);
if is_enabled
&& compilation.build_chunk_graph_artifact.chunk_graph
.has_chunk_entry_dependent_chunks(chunk_ukey, &...chunk_group_by_ukey)
{
runtime_requirements.insert(RuntimeGlobals::ASYNC_STARTUP);
}
Ok(())
}
有 entry 依赖的 chunk 需要 ASYNC_STARTUP——因为它启动时要等异步 chunk 加载完。
runtime_requirement_in_tree:按需注入
#[plugin_hook(CompilationRuntimeRequirementInTree for JsonpChunkLoadingPlugin)]
async fn runtime_requirements_in_tree(&self, compilation, chunk_ukey, all_runtime_requirements, runtime_requirements, runtime_requirements_mut, runtime_modules_to_add) {
// ...
let has_chunk_loading_runtime_globals = RuntimeGlobals::ENSURE_CHUNK_HANDLERS
| RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS
| RuntimeGlobals::HMR_DOWNLOAD_MANIFEST
| RuntimeGlobals::ON_CHUNKS_LOADED
| RuntimeGlobals::BASE_URI;
if runtime_requirements.intersects(has_chunk_loading_runtime_globals) {
runtime_modules_to_add.push((
*chunk_ukey,
JsonpChunkLoadingRuntimeModule::new(&compilation.runtime_template).boxed(),
));
}
// ...
}
只有当 chunk 确实需要「加载其他 chunk」的能力(ENSURE_CHUNK_HANDLERS 等)时,才注入 JsonpChunkLoadingRuntimeModule。
runtime requirements 的级联
注入 runtime module 后,它自己还会带来新的 requirement:
if all_runtime_requirements.contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS) {
runtime_requirements_mut.extend(JsonpChunkLoadingRuntimeModule::get_runtime_requirements_basic());
if all_runtime_requirements.contains(RuntimeGlobals::PREFETCH_CHUNK_HANDLERS) {
runtime_requirements_mut.extend(...get_runtime_requirements_with_prefetch());
}
if all_runtime_requirements.contains(RuntimeGlobals::PRELOAD_CHUNK_HANDLERS) {
runtime_requirements_mut.extend(...get_runtime_requirements_with_preload());
}
}
// HMR 相关的 requirement 级联
if all_runtime_requirements.contains(RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS) {
runtime_requirements_mut.extend(...get_runtime_requirements_with_hmr());
}
// ...
这就是第 9 篇说的「runtime requirement 是迭代传播的」:一个 requirement 触发 runtime module,runtime module 又带来新的 requirement,直到收敛。
源码入口:EnsureChunkRuntimeModule
crates/rspack_plugin_runtime/src/runtime_module/ensure_chunk.rs 是「按需加载 chunk」的运行时实现:
#[impl_runtime_module]
#[derive(Debug)]
pub struct EnsureChunkRuntimeModule {
has_async_chunks: bool,
}
async fn generate(&self, context) -> Result<String> {
let runtime_requirements = get_chunk_runtime_requirements(compilation, &chunk_ukey);
let source = if runtime_requirements.contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS) {
let fetch_priority = if runtime_requirements.contains(RuntimeGlobals::HAS_FETCH_PRIORITY) {
", fetchPriority"
} else { "" };
runtime_template.render(&self.template_id(TemplateId::Raw), Some(json!({"_fetch_priority": fetch_priority})))?
} else {
runtime_template.render(&self.template_id(TemplateId::WithInline), None)?
};
Ok(source)
}
模板通过 include_str! 内联:
fn template(&self) -> Vec<(String, String)> {
vec![
(self.template_id(TemplateId::Raw), include_str!("runtime/ensure_chunk.ejs").to_string()),
(self.template_id(TemplateId::WithInline), include_str!("runtime/ensure_chunk_with_inline.ejs").to_string()),
]
}
ensure_chunk.ejs 是 EJS 模板,运行时用 serde_json 传参渲染。这是「模板 + 数据」的运行时生成模式。
additional_runtime_requirements
runtime module 也能声明自己需要的 requirement:
fn additional_runtime_requirements(&self, _compilation: &Compilation) -> RuntimeGlobals {
if self.has_async_chunks {
RuntimeGlobals::ENSURE_CHUNK_HANDLERS
} else {
RuntimeGlobals::default()
}
}
有异步 chunk 时,EnsureChunkRuntimeModule 需要 ENSURE_CHUNK_HANDLERS 这个 global。
源码入口:ModuleChunkFormatPlugin
crates/rspack_plugin_runtime/src/module_chunk_format.rs 是 output.module(原生 ESM)的实现。它的 render_chunk 把模块代码包成 ESM:
#[plugin_hook(JavascriptModulesRenderChunk for ModuleChunkFormatPlugin)]
async fn render_chunk(&self, compilation, chunk_ukey, render_source, runtime_template) {
let chunk_id_json_string = json_stringify(chunk.expect_id());
let mut sources = ConcatSource::default();
sources.add(RawStringSource::from(format!(
"export const __rspack_esm_id = {chunk_id_json_string};\n"
)));
sources.add(RawStringSource::from(format!(
"export const __rspack_esm_ids = [{chunk_id_json_string}];\n"
)));
sources.add(RawStringSource::from(format!(
"export const {} = ",
runtime_template.render_runtime_variable(&RuntimeVariable::Modules),
)));
sources.add(render_source.source.clone());
sources.add(RawStringSource::from_static(";\n"));
// ...
}
每个 chunk 导出 __rspack_esm_id、__rspack_esm_ids 和 __rspack_modules,供 ESM 加载器识别。
ESM 的 chunk 加载
ESM chunk 之间用原生 import 连接:
startup_source.push(format!(
"import * as __rspack_chunk_{} from '{}';",
index_str,
get_relative_path(&base_chunk_output_name, &other_chunk_output_name)
));
startup_source.push(format!(
"{}(__rspack_chunk_{});",
runtime_template.render_runtime_globals(&RuntimeGlobals::EXTERNAL_INSTALL_CHUNK),
index_str2
));
原生 ESM 用 import * as chunk_X from './chunk.js' 加载依赖 chunk,再用 EXTERNAL_INSTALL_CHUNK 安装它。
render_startup:依赖 chunk 的加载
render_startup 处理 entry chunk 的依赖:
#[plugin_hook(JavascriptModulesRenderStartup for ModuleChunkFormatPlugin)]
async fn render_startup(&self, compilation, chunk_ukey, _module, render_source, runtime_template) {
// ...
let dependent_chunks = compilation.build_chunk_graph_artifact.chunk_graph
.get_chunk_entry_dependent_chunks_iterable(...);
for (index, ck) in dependent_chunks.enumerate() {
let named_import = format!("__rspack_imports_{index}");
// ...
dependent_load.add(RawStringSource::from(render_chunk_import(&named_import, &imported)));
dependent_load.add(RawStringSource::from(format!(
"{}({});\n",
runtime_template.render_runtime_globals(&RuntimeGlobals::EXTERNAL_INSTALL_CHUNK),
named_import
)));
}
// ...
}
chunk_has_js 与 chunk_has_css
lib.rs 还导出了两个常用判断:
pub use runtime_module::{EXPORT_REQUIRE_RUNTIME_MODULE_ID, GetChunkFilenameRuntimeModule, chunk_has_css, is_enabled_for_chunk, stringify_chunks};
chunk_has_js(在 module_chunk_format 里用到)判断 chunk 是否有 JS,chunk_has_css 判断是否有 CSS。这些是渲染时的过滤条件。
Rust 角度:runtime 是「需求驱动」的组装
rspack_plugin_runtime 的核心设计是:
配置 -> 选择 chunk loading 插件
chunk 图 -> 标记 runtime requirement
requirement -> 注入 runtime module
runtime module -> 级联新 requirement
整个过程是「需求驱动」而非「全量注入」。一个纯同步、无异步 chunk 的项目,不会注入 jsonp 加载代码;一个 Node 项目不会注入 script 标签加载代码。这保证了产物里只有「真正需要的 runtime」。
这一篇应该带走什么
enable_chunk_loading_plugin按 5 种加载类型选择插件;- chunk loading 插件 tap
additional_tree_runtime_requirements和runtime_requirement_in_tree两个 Hook; - runtime requirement 迭代传播,直到收敛;
EnsureChunkRuntimeModule用 EJS 模板 + serde_json 数据渲染;ModuleChunkFormatPlugin为output.module生成原生 ESM 产物。
写在最后
runtime 是产物的「操作系统」。接下来进入一个完全不同的层次——Module Federation。它把「加载本地 chunk」扩展成「加载远程容器里的模块」,核心是 RemoteModule 和 ContainerEntryModule。下一篇进入 rspack_plugin_mf,看远程模块如何被表示和加载。
把「按需生成 runtime」的抽象讲透了,两条 Hook 加需求级联的思路值得借鉴,适合想深入打包器插件架构与产物裁剪的进阶读者。