Rspack 源码解析(三十三):Module Federation 如何用 RemoteModule 与 ContainerEntryModule 表示远程代

本篇是 Rspack 源码解析系列第三十三篇。第二十二篇 externals 末尾说 remote 和 externals 是同一思路的两种形态。本文进入 rspack_plugin_mf,看 Module Federation 的 RemoteModule(消费远程)和 ContainerEntryModule(暴露本地)如何实现,把「加载远程代码」变成一种模块类型。

前言:远程模块不是真实模块

Module Federation 的核心是:

text 复制代码
宿主应用 import 'remote/Button'
  -> 运行时从远程容器加载 Button
  -> 而不是本地打包 Button

这和 externals(第 22 篇)的「假模块代替真实模块」一脉相承。区别是 externals 从全局变量/require 取,remote 从远程容器的 get/init 接口取。

源码入口:ContainerPlugin 暴露本地

crates/rspack_plugin_mf/src/container/container_plugin.rs 的 ContainerPlugin 负责「我暴露什么给别人」。它的 make Hook 创建一个 ContainerEntryDependency 并 add_entry:

rust 复制代码
#[plugin_hook(CompilerMake for ContainerPlugin)]
async fn make(&self, compilation: &mut Compilation) -> Result<()> {
  let dep = ContainerEntryDependency::new(
    self.options.name.clone(),
    self.options.exposes.clone(),
    self.options.share_scope.clone(),
    self.options.enhanced,
  );

  compilation.add_entry(
    Box::new(dep),
    EntryOptions {
      name: Some(self.options.name.clone()),
      runtime: self.options.runtime.clone(),
      filename: self.options.filename.clone(),
      library: Some(self.options.library.clone()),
      ..Default::default()
    },
  ).await?;
  Ok(())
}

ContainerEntryDependency 不是普通 entry,它会走 ContainerEntryModuleFactory:

rust 复制代码
#[plugin_hook(CompilerCompilation for ContainerPlugin)]
async fn compilation(&self, compilation, params) -> Result<()> {
  compilation.set_dependency_factory(DependencyType::ContainerEntry, Arc::new(ContainerEntryModuleFactory));
  compilation.set_dependency_factory(DependencyType::ContainerExposed, params.normal_module_factory.clone());
  Ok(())
}

ContainerEntryModule:暴露的容器

crates/rspack_plugin_mf/src/container/container_entry_module.rs 的 ContainerEntryModule 是「容器入口」。它的 build 为每个 expose 创建异步 block:

rust 复制代码
async fn build(mut self: Box<Self>, _build_context, _: Option<&Compilation>) -> Result<BuildResult> {
  let mut blocks = vec![];
  let mut dependencies: Vec<BoxDependency> = vec![];

  // Container 逻辑
  for (name, options) in &self.exposes {
    let mut block = AsyncDependenciesBlock::new(
      self.identifier,
      None,
      Some(name),
      options.import.iter().map(|request| {
        Box::new(ContainerExposedDependency::new(name.clone(), request.clone())) as Box<dyn Dependency>
      }).collect(),
      None,
    );
    block.set_group_options(GroupOptions::ChunkGroup(
      ChunkGroupOptions::default().name_optional(options.name.clone()),
    ));
    blocks.push(Box::new(block));
  }
  dependencies.push(Box::new(StaticExportsDependency::new(
    StaticExportsSpec::Array(vec!["get".into(), "init".into()]),
    false,
  )));
  // ...
}

每个 expose 对应一个 AsyncDependenciesBlock(异步块),因为远程加载是异步的。容器导出两个方法:get 和 init。

code_generation:生成 get/init

ContainerEntryModule 的 code_generation 生成容器的运行时代码。核心是 moduleMap:

rust 复制代码
let module_map = ExposeModuleMap::new(compilation, self, runtime_template);
let module_map_str = module_map.render(runtime_template);

ExposeModuleMap 把每个 expose 名映射到「加载该模块的 factory」:

rust 复制代码
impl ExposeModuleMap {
  pub fn new(compilation, container_entry_module, runtime_template) -> Self {
    let mut module_map = vec![];
    for block_id in container_entry_module.get_blocks() {
      // ...
      let str = if modules_iter.clone().any(|(_, module, _, _)| module.is_none()) {
        runtime_template.throw_missing_module_error_block(...)
      } else {
        let block_promise = runtime_template.block_promise(Some(block_id), compilation, "");
        let modules = modules_iter.map(|(_, _, request, dependency_id)| {
          runtime_template.module_raw(compilation, dependency_id, request, false)
        }).collect::<Vec<_>>().join(", ");
        let module_raw = runtime_template.returning_function(
          &runtime_template.returning_function(&modules, ""), "");
        format!("return {block_promise}.then({module_raw});")
      };
      module_map.push((name.to_string(), str));
    }
    Self(module_map)
  }
}

每个 expose 的 value 是 return blockPromise.then(module => () => module)------先异步加载 block,再返回一个返回模块的函数。

生成的 get/init 代码

code_generation 生成的代码(非 enhanced 模式):

rust 复制代码
let source = format!(
  r#"
var moduleMap = {module_map_str};
var get = function(module, getScope) {{
  {current_remote_get_scope} = getScope;
  getScope = (
    {has_own_property}(moduleMap, module)
      ? moduleMap[module]()
      : Promise.resolve().then({get_scope_reject})
  );
  {current_remote_get_scope} = undefined;
  return getScope;
}}
var init = function(shareScope, initScope) {{
  if (!{share_scope_map}) return;
  var name = {share_scope};
  var oldScope = {share_scope_map}[name];
  if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
  {share_scope_map}[name] = shareScope;
  return {initialize_sharing}(name, initScope);
}}
{define_property_getters}({exports}, {{
  get: {export_get},
  init: {export_init}
}});"#,
  // ...
);

get 从 moduleMap 取模块(异步),init 初始化 share scope(共享依赖)。define_property_getters 让 get/init 成为只读 getter。

源码入口:ContainerReferencePlugin 消费远程

container_reference_plugin.rs 的 ContainerReferencePlugin 负责「我要消费哪个远程」。它在 factorize Hook 里拦截 remote/xxx 请求,返回 RemoteModule。

RemoteModule:远程模块

crates/rspack_plugin_mf/src/container/remote_module.rs 的 RemoteModule 是「远程模块」的表示:

rust 复制代码
#[cacheable]
#[derive(Debug)]
pub struct RemoteModule {
  // ...
  request: String,
  external_requests: Vec<String>,
  pub internal_request: String,
  pub share_scope: String,
  pub remote_key: String,
  // ...
}

它的 build 把远程模块转成「从远程容器加载」的依赖:

rust 复制代码
async fn build(mut self: Box<Self>, build_context: BuildContext, _: Option<&Compilation>) -> Result<BuildResult> {
  let mut dependencies: Vec<BoxDependency> = Vec::new();

  if self.external_requests.len() == 1 {
    let dep = RemoteToExternalDependency::new(self.external_requests[0].clone());
    // ...
    dependencies.push(Box::new(dep));
  } else {
    let dep = FallbackDependency::new(self.external_requests.clone());
    // ...
    dependencies.push(Box::new(dep));
  }
  // ...
}

单个远程请求转 RemoteToExternalDependency,多个(带 fallback)转 FallbackDependency。

code_generation:从远程容器取

RemoteModule 的 code_generation 很轻:

rust 复制代码
async fn code_generation(&self, code_generation_context) -> Result<CodeGenerationResult> {
  let mut codegen = CodeGenerationResult::default();
  let module_graph = code_generation_context.compilation.get_module_graph();
  let module = module_graph.get_module_by_dependency_id(&self.dependencies[0]);
  let id = module.and_then(|m| ChunkGraph::get_module_id(...));
  codegen.add(SourceType::Remote, RawStringSource::from_static("").boxed());
  codegen.data.insert(CodeGenerationDataShareInit {
    items: vec![ShareInitData {
      share_scope: self.share_scope.clone(),
      init_stage: 20,
      init: DataInitInfo::ExternalModuleId(id.cloned()),
    }],
  });
  Ok(codegen)
}

RemoteModule 自己不生成 JS,而是通过 CodeGenerationDataShareInit 声明「需要初始化 share scope,并引用某个外部模块的 id」。真正的加载逻辑在 remote_runtime_module 和 remotesLoading.ejs 里。

source_types:Remote 与 ShareInit

RemoteModule 的 source types 是特殊的:

rust 复制代码
fn source_types(&self, _module_graph: &ModuleGraph) -> &[SourceType] {
  &[SourceType::Remote, SourceType::ShareInit]
}

Remote 类型对应远程加载代码,ShareInit 对应 share scope 初始化。这又是 source type 决定 code generation 的体现。

源码入口:ConsumeSharedModule 共享依赖

crates/rspack_plugin_mf/src/sharing/consume_shared_module.rs 的 ConsumeSharedModule 表示「消费共享模块」(如 shared 的 react)。

它的 code_generation 决定调用哪个 loader 函数:

rust 复制代码
let mut function = String::from("loaders.load");
let mut args = vec![
  json_stringify(&self.options.share_scope),
  json_stringify(&self.options.share_key),
];
if let Some(version) = &self.options.required_version {
  if self.options.strict_version {
    function += "Strict";
  }
  if self.options.singleton {
    function += "Singleton";
  }
  let version = json_stringify(&version.to_string());
  args.push(format!("loaders.parseRange({version})"));
  function += "VersionCheck";
} else if self.options.singleton {
  function += "Singleton";
}

根据 strict_version、singleton、required_version 组合出不同的 loader 函数名:load、loadStrict、loadSingleton、loadVersionCheck、loadStrictSingletonVersionCheck 等。

ConsumeSharedPlugin:factorize 拦截

consume_shared_plugin.rs 的 factorize Hook 拦截 shared 请求:

rust 复制代码
#[plugin_hook(NormalModuleFactoryFactorize for ConsumeSharedPlugin)]
async fn factorize(&self, data: &mut ModuleFactoryCreateData) -> Result<Option<BoxModule>> {
  // 跳过 fallback 相关依赖
  if matches!(dep.dependency_type(), DependencyType::ConsumeSharedFallback | ...) {
    return Ok(None);
  }
  let request = &data.request;
  let consumes = self.get_matched_consumes();

  if let Some(matched) = consumes.unresolved.get(request) {
    let module = self.create_consume_shared_module(&data.context, request, matched.clone(), ...).await;
    return Ok(Some(module.boxed()));
  }
  // 前缀匹配
  for (prefix, options) in &consumes.prefixed {
    if request.starts_with(prefix) {
      // ...
    }
  }
  Ok(None)
}

这和 externals 的 factorize 拦截是同一个模式:在模块创建前拦截,返回替代模块。

resolve_matched_configs:匹配 shared 配置

shared 配置的 request 分三类:

rust 复制代码
for (request, config) in configs {
  if RELATIVE_REQUEST.is_match(request) {
    // ./xxx 相对路径 -> resolver 解析
  } else if ABSOLUTE_REQUEST.is_match(request) {
    // /xxx 绝对路径
  } else if request.ends_with('/') {
    // 前缀匹配
    prefixed.insert(request.to_owned(), config.clone());
  } else {
    // 裸包名,运行时解析
    unresolved.insert(request.to_owned(), config.clone());
  }
}

unresolved 的是包名(如 react),它们在运行时才真正解析版本,所以走 factorize 拦截;resolved 的是已解析路径,走 create_module 拦截。

Rust 角度:模块类型是扩展点

Module Federation 展示了「模块类型」作为扩展点的威力:

text 复制代码
ContainerEntryModule   -> SourceType::JavaScript + Expose
RemoteModule           -> SourceType::Remote + ShareInit
ConsumeSharedModule    -> SourceType::ConsumeShared

每一种「特殊加载方式」都对应一种 Module 实现,通过 source_types 声明自己需要什么 code generation,通过 set_dependency_factory 注册依赖到模块的转换。整个模块图体系不需要为 MF 做特殊分支。

这一篇应该带走什么

  1. ContainerPlugin 暴露本地,ContainerEntryModule 生成 get/init 接口;
  2. ContainerReferencePlugin 消费远程,RemoteModule 表示远程模块;
  3. remote 的加载逻辑不在模块里,而在 runtime module + remotesLoading.ejs;
  4. ConsumeSharedModule 根据 strict/singleton/version 组合 loader 函数名;
  5. MF 通过「新模块类型 + dependency factory」扩展,不侵入模块图核心。

写在最后

MF 是「模块类型扩展」的极致。接下来回到 Rust 侧的「宏」------整个插件体系大量使用 #[plugin]、#[plugin_hook]、#[cacheable] 宏,但它们的实现细节一直没展开。下一篇进入 rspack_hook 与 rspack_macros,看 Hook 系统如何用宏生成异步插桩代码。

相关推荐
特立独行的猫A1 小时前
仓颉语言实现的Harness: 一口气派三个子代理,cjh 的并发不是“多线程“,是“进程树“
前端·harmonyos
Dumbness1 小时前
农历转换的 4 个边界:2 月 30 日能转出"正月十四"、小月没有三十、闰月、立春到春节之间的干支
前端·javascript
不可能片场1 小时前
工程实践 日志文件名是北京日期 内容却是 UTC
前端·electron
徐小夕1 小时前
JitWord 4.3 发布!Word 内嵌 Visio 图表、原生流程图一键无损解析
前端·vue.js·github
挖掘狂人1 小时前
全栈正在杀死前端?被淘汰的从来不是岗位,只会是执行层
前端·ai编程·全栈
sycmancia1 小时前
Qml——Button元素
前端
codigger1 小时前
前端没有被杀死,被杀死的是"只会把设计稿变成页面"
前端·ai·ai编程·编程语言·全栈·#人工智能
艺杯羹2 小时前
本地优先架构避坑:从重度云端依赖到客户端算力下沉与边缘自治实战
前端·系统架构·本地优先
transformer2 小时前
用 TRAE 给 Git 教程站补上第七个模块:一次真实的增量开发记录
前端