Module Federation 的核心是 在多个独立构建之间建立运行时模块共享机制。下面从构建时产物和运行时加载两个阶段,结合 Webpack 源码逻辑,详细拆解其原理。
一、整体设计思路
-
构建阶段:每个参与联邦的应用(容器)都会生成一个特殊的入口文件(
remoteEntry.js),里面包含了暴露模块的映射表、共享依赖的初始化函数以及动态加载模块的方法。 -
运行时阶段:消费方通过动态脚本加载远程容器的入口,然后调用其
init方法初始化共享作用域,再通过get方法按需加载远程模块。
核心代码位于 Webpack 源码中的 lib/container 目录,关键类包括:
-
ContainerPlugin:为当前应用生成容器(暴露方) -
ContainerReferencePlugin:引用远程容器(消费方) -
ContainerEntryModule/RemoteModule:容器和远程模块的表示
二、构建阶段的关键行为
1. 暴露方(ModuleFederationPlugin 配置 exposes)
假设配置:
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/Button'
},
shared: ['react', 'react-dom']
})
Webpack 内部处理流程(源码简化描述):
-
ContainerPlugin创建一个容器入口模块(ContainerEntryModule),该模块会生成一个运行时对象,包含:-
__webpack_require__.initContainer:初始化共享作用域 -
__webpack_require__.getContainer:获取暴露的模块
-
-
对于每个暴露项,生成一个
ContainerEntryModule的子模块,它实际引用原始模块路径,并记录暴露名称./Button。 -
生成
remoteEntry.js,其核心代码类似于:// remoteEntry.js (简化) var moduleMap = { "./Button": () => import("./src/Button") };
var shareScopeMap = {};
async function init(shareScope) { shareScopeMap = shareScope; // 共享依赖的版本检查和初始化 }
async function get(moduleId) { if (!moduleMap[moduleId]) throw new Error("Module not found"); const factory = await moduleMapmoduleId; const module = factory(); // 应用共享作用域到模块(处理 shared 依赖) return module; }
export { init, get };
实际上 Webpack 生成的代码会更复杂,包含 shareScope 的挂载和远程模块的代理。
2. 消费方(remotes 配置)
new ModuleFederationPlugin({
name: 'app2',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
},
shared: ['react', 'react-dom']
})
处理逻辑:
-
ContainerReferencePlugin创建一个 远程模块(RemoteModule),它是一个动态代理模块,并不会在构建时包含远程模块的内容。 -
对于代码中的
import('app1/Button'),Webpack 会将其编译成运行时调用__webpack_require__.remotes映射表,最终调用内部函数loadRemote。 -
生成的消费方运行时代码大致如下:
// 消费方 bundle 中注入的远程加载逻辑 __webpack_require__.remotes = { app1: { async get(modulePath) { const container = await __webpack_require__.l("http://localhost:3001/remoteEntry.js"); await container.init(__webpack_require__.shareScope); return container.get(modulePath); } } }; // 当执行 import('app1/Button') 时 var moduleFactory = __webpack_require__.remotes.app1; var Button = await moduleFactory.get('./Button');
三、运行时核心源码解析(模拟关键函数)
Webpack 运行时内部会注入一组工具函数(runtime module),主要用于处理异步脚本加载和共享作用域管理。下面以 Webpack 5 实际生成的 runtime 代码(简化但核心逻辑一致)为例。
1. 加载远程入口 __webpack_require__.l
__webpack_require__.l = function(url, done) {
var script = document.createElement('script');
script.src = url;
script.onload = function() {
// 远程入口执行后会暴露一个全局变量,名称为配置中的 name (如 'app1')
var container = window['app1'];
done(container);
};
document.head.appendChild(script);
};
2. 初始化共享作用域 init
远程容器的 init 方法接收消费方传递过来的共享依赖映射:
// 远程容器中的 init 伪代码
var shareScope = {}; // 容器内部的共享作用域
function init(consumerShareScope) {
// 合并共享依赖:消费者提供的优先,并保证 singleton 版本一致性
for (var key in consumerShareScope) {
if (shareScope[key]) {
// 版本冲突时,按照规则选择(例如使用更高版本或拒绝)
if (!versionMatch(shareScope[key], consumerShareScope[key])) {
throw new Error('Version mismatch');
}
} else {
shareScope[key] = consumerShareScope[key];
}
}
// 执行所有共享模块的初始化(如 React 的 singleton 实例)
return Promise.resolve();
}
3. 获取远程模块 get
// 远程容器中的 get 伪代码
function get(modulePath) {
// moduleMap 是暴露的模块加载器映射
var moduleLoader = moduleMap[modulePath];
if (!moduleLoader) return Promise.reject();
return moduleLoader().then(function(moduleFactory) {
// moduleFactory 是原始模块经过 webpack 包装后的函数
// 使用共享作用域中的依赖替换模块内部的 require
return __webpack_require__.bind(shareScope)(moduleFactory);
});
}
4. 消费方完整调用链示例
// 消费方代码
const Button = await import('app1/Button');
// 编译后的实际代码
var remoteContainer = __webpack_require__.remotes['app1'];
var remoteModule = await remoteContainer.get('./Button');
var Button = remoteModule.default; // 或按需获取
四、共享依赖(shared)的精确控制
shared 的配置决定了依赖如何在不同容器之间共享。核心源码逻辑在 SharePlugin 和 ConsumeSharedPlugin 中。
关键点:
-
每个容器会维护一个 全局共享作用域(
__webpack_share_scopes__)。 -
消费方在加载远程容器之前,会将自己的共享作用域通过
init传给远程容器。 -
远程容器暴露的模块内,原本
require('react')会被重写为从共享作用域中获取,从而保证单例。
例如,远程容器中暴露的 Button.js 代码原本是:
import React from 'react';
export default () => <div />;
经过 Module Federation 处理后的实际代码变为:
import { getShared } from "__webpack_require__";
var React = getShared('react', { version: '^17.0.0' });
export default () => <div />;
五、完整逻辑
-
构建时:
-
暴露方生成
remoteEntry.js,包含init和get方法,以及moduleMap(暴露模块的异步加载器)。 -
消费方生成
remotes映射,将app1/Button这类引用转换为运行时加载指令。
-
-
运行时:
-
消费方首次
import('app1/Button')。 -
Webpack 调用内部
loadRemote,动态插入<script>加载http://localhost:3001/remoteEntry.js。 -
远程入口执行,将自身容器对象挂载到
window['app1']。 -
消费方调用
window['app1'].init(consumerShareScope),合并共享依赖。 -
消费方调用
window['app1'].get('./Button')。 -
远程容器从
moduleMap中找到对应加载器,import('./src/Button')加载真实模块。 -
远程容器将模块工厂函数用共享作用域执行,返回模块导出。
-
消费方得到模块,正常使用。
-
六、总结
-
本质:Module Federation 是一种运行时依赖注入方案,将传统的构建时
import推迟到运行时,通过动态加载远程入口和共享作用域协作完成模块解析。 -
关键技术:动态脚本注入、共享作用域(全局状态)、代理模块(remote module)和模块工厂函数的重写。
-
源码核心位置:
@webpack/lib/container/、@webpack/lib/sharing/,尤其是ContainerPlugin.js、ContainerReferencePlugin.js、ConsumeSharedPlugin.js。
通过上述机制,Module Federation 实现了真正的独立部署、运行时集成的微前端模块共享能力,而无需任何中心化构建流程。