Rspack 源码解析(三十二):rspack_plugin_runtime 如何抽象 chunk 加载与启动

文章来源声明: 原文作者:Amos_zz; 来源站点:掘金; 原文链接:https://juejin.cn/post/7700891797551349770; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

把「按需生成 runtime」的抽象讲透了,两条 Hook 加需求级联的思路值得借鉴,适合想深入打包器插件架构与产物裁剪的进阶读者。

> 本篇是 Rspack 源码解析系列第三十二篇。第九篇讲 runtime requirements 时提到「runtime 代码按需生成」。本文进入 `rspack_plugin_runtime`,看 runtime 代码到底由哪些 plugin 和 runtime module 组成,以及 chunk 加载(jsonp/require/import)如何被抽象。

前言: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(_) => (),
  }
}

五种加载方式对应五种插件:

类型插件场景
JsonpJsonpChunkLoadingPlugin浏览器(script 标签)
RequireCommonJsChunkLoadingPlugin(false)Node(同步 require)
AsyncNodeCommonJsChunkLoadingPlugin(true)Node(异步 import)
ImportScriptsImportScriptsChunkLoadingPluginWeb Worker
ImportModuleChunkLoadingPlugin原生 ESM

两个核心 Hook

每个 chunk loading 插件都 tap 两个 Hook:

  1. CompilationAdditionalTreeRuntimeRequirements:在 chunk 图里标记 runtime requirement;
  2. 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」。

这一篇应该带走什么

  1. enable_chunk_loading_plugin 按 5 种加载类型选择插件;
  2. chunk loading 插件 tap additional_tree_runtime_requirements 和 runtime_requirement_in_tree 两个 Hook;
  3. runtime requirement 迭代传播,直到收敛;
  4. EnsureChunkRuntimeModule 用 EJS 模板 + serde_json 数据渲染;
  5. ModuleChunkFormatPlugin 为 output.module 生成原生 ESM 产物。

写在最后

runtime 是产物的「操作系统」。接下来进入一个完全不同的层次——Module Federation。它把「加载本地 chunk」扩展成「加载远程容器里的模块」,核心是 RemoteModule 和 ContainerEntryModule。下一篇进入 rspack_plugin_mf,看远程模块如何被表示和加载。