本篇是 Rspack 源码解析系列第三十三篇。第二十二篇 externals 末尾说 remote 和 externals 是同一思路的两种形态。本文进入
rspack_plugin_mf,看 Module Federation 的RemoteModule(消费远程)和ContainerEntryModule(暴露本地)如何实现,把「加载远程代码」变成一种模块类型。
前言:远程模块不是真实模块
Module Federation 的核心是:
宿主应用 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:
#[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:
#[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:
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:
let module_map = ExposeModuleMap::new(compilation, self, runtime_template);
let module_map_str = module_map.render(runtime_template);
ExposeModuleMap 把每个 expose 名映射到「加载该模块的 factory」:
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 模式):
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 是「远程模块」的表示:
#[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 把远程模块转成「从远程容器加载」的依赖:
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 很轻:
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 是特殊的:
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 函数:
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 请求:
#[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 分三类:
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 展示了「模块类型」作为扩展点的威力:
ContainerEntryModule -> SourceType::JavaScript + Expose
RemoteModule -> SourceType::Remote + ShareInit
ConsumeSharedModule -> SourceType::ConsumeShared
每一种「特殊加载方式」都对应一种 Module 实现,通过 source_types 声明自己需要什么 code generation,通过 set_dependency_factory 注册依赖到模块的转换。整个模块图体系不需要为 MF 做特殊分支。
这一篇应该带走什么
ContainerPlugin暴露本地,ContainerEntryModule生成get/init接口;ContainerReferencePlugin消费远程,RemoteModule表示远程模块;- remote 的加载逻辑不在模块里,而在 runtime module +
remotesLoading.ejs; ConsumeSharedModule根据 strict/singleton/version 组合 loader 函数名;- MF 通过「新模块类型 + dependency factory」扩展,不侵入模块图核心。
写在最后
MF 是「模块类型扩展」的极致。接下来回到 Rust 侧的「宏」——整个插件体系大量使用 #[plugin]、#[plugin_hook]、#[cacheable] 宏,但它们的实现细节一直没展开。下一篇进入 rspack_hook 与 rspack_macros,看 Hook 系统如何用宏生成异步插桩代码。