Module Federation 模块联邦
问题
什么是 Module Federation?它是如何实现跨应用代码共享的?运行时加载远程模块的原理是什么?它与 qiankun 等微前端方案有何区别?
答案
Module Federation(模块联邦) 是 Webpack 5 引入的一项革命性特性,它允许多个独立构建的应用在运行时动态共享代码。不同于传统的 npm 包共享方式(构建时静态依赖),Module Federation 让一个应用可以直接加载另一个应用暴露出的模块,实现真正的跨应用代码共享。
Module Federation 解决了微前端架构中最棘手的问题:如何在不同独立部署的应用之间高效地共享代码和依赖,同时避免重复打包和版本冲突。它让每个应用既是独立的,又能在运行时组合为一个整体。
核心概念
Module Federation 的架构围绕几个核心概念展开:
Host、Remote 与 Container
- Remote(提供者/远程应用):暴露(exposes)自身模块供其他应用使用的应用
- Host(消费者/宿主应用):消费(remotes)其他应用暴露模块的应用
- Container(容器):每个使用了 Module Federation 的应用都会生成一个容器,容器是一个异步的 JavaScript 入口文件,包含了该应用暴露的所有模块的引用
- Shared(共享依赖):多个应用之间共享的公共依赖(如 React、Vue),避免重复加载
一个应用可以同时作为 Host 和 Remote。例如,应用 A 暴露了组件库供应用 B 使用,同时应用 A 也消费了应用 C 暴露的工具函数。
架构图
ModuleFederationPlugin 配置详解
Module Federation 通过 Webpack 5 内置的 ModuleFederationPlugin 插件实现。
核心配置字段
| 字段 | 类型 | 说明 |
|---|---|---|
name | string | 容器名称,全局唯一,作为全局变量暴露 |
filename | string | 容器入口文件名,通常为 remoteEntry.js |
exposes | object | 暴露给其他应用使用的模块映射 |
remotes | object | 声明要消费的远程应用及其入口 |
shared | object | array | 声明需要共享的依赖及其策略 |
Remote 端配置(提供者)
import { Configuration, container } from 'webpack';
const { ModuleFederationPlugin } = container;
const config: Configuration = {
// ...其他配置
plugins: [
new ModuleFederationPlugin({
// 容器名称,全局唯一,Host 端通过此名称引用
name: 'remoteApp',
// 容器入口文件名,Host 需要加载这个文件
filename: 'remoteEntry.js',
// 暴露的模块:key 是对外暴露的路径,value 是本地模块路径
exposes: {
'./Button': './src/components/Button',
'./Modal': './src/components/Modal',
'./utils': './src/utils/index',
},
// 共享依赖配置
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
export default config;
Host 端配置(消费者)
import { Configuration, container } from 'webpack';
const { ModuleFederationPlugin } = container;
const config: Configuration = {
// ...其他配置
plugins: [
new ModuleFederationPlugin({
name: 'hostApp',
// 声明远程应用:key 是本地引用名,value 格式为 "name@url"
remotes: {
// 格式:远程容器名@远程入口 URL
remoteApp: 'remoteApp@http://localhost:3001/remoteEntry.js',
anotherApp: 'anotherApp@http://localhost:3002/remoteEntry.js',
},
// Host 也需要声明共享依赖
shared: {
react: {
singleton: true,
requiredVersion: '^18.2.0',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
},
}),
],
};
export default config;
在 Host 中使用远程模块
import React, { lazy, Suspense } from 'react';
// 使用动态 import 加载远程模块
// 路径格式:remotes 配置的 key / exposes 配置的 key
const RemoteButton = lazy(() => import('remoteApp/Button'));
const RemoteModal = lazy(() => import('remoteApp/Modal'));
const App: React.FC = () => {
return (
<div>
<h1>Host 应用</h1>
{/* 远程模块需要 Suspense 包裹,因为是异步加载 */}
<Suspense fallback={<div>Loading remote component...</div>}>
<RemoteButton label="来自远程应用的按钮" />
<RemoteModal title="远程弹窗" />
</Suspense>
</div>
);
};
export default App;
远程模块默认没有 TypeScript 类型声明,需要手动创建 .d.ts 文件或使用 @module-federation/typescript 插件自动生成:
declare module 'remoteApp/Button' {
import { FC } from 'react';
interface ButtonProps {
label: string;
onClick?: () => void;
}
const Button: FC<ButtonProps>;
export default Button;
}
declare module 'remoteApp/Modal' {
import { FC } from 'react';
interface ModalProps {
title: string;
visible?: boolean;
onClose?: () => void;
}
const Modal: FC<ModalProps>;
export default Modal;
}
共享依赖配置详解
共享依赖(shared)是 Module Federation 最关键的配置之一,它决定了多个应用之间如何复用公共依赖:
const shared = {
react: {
// singleton: 单例模式,确保整个页面只有一份 React 实例
singleton: true,
// requiredVersion: 声明要求的版本范围
requiredVersion: '^18.2.0',
// eager: 是否立即加载(不异步),入口文件需要时设为 true
eager: false,
// strictVersion: 版本不匹配时是否抛出错误(默认 warning)
strictVersion: false,
// shareKey: 共享作用域中的 key,默认和包名一致
shareKey: 'react',
// shareScope: 共享作用域名称,默认 'default'
shareScope: 'default',
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
// 简写形式:直接写包名,使用默认配置
lodash: '^4.17.0',
};
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
singleton | boolean | false | 是否为单例,确保全局只有一份实例 |
requiredVersion | string | package.json 中的版本 | 要求的语义化版本范围 |
eager | boolean | false | 是否立即加载而非异步 |
strictVersion | boolean | false | 版本不匹配时是否报错(否则仅 warning) |
shareKey | string | 包名 | 在共享作用域中的标识 |
shareScope | string | 'default' | 共享作用域名称 |
像 React 这样的库必须设置 singleton: true,因为 React 内部依赖全局状态(如 Hooks 链表)。如果页面上存在两个不同的 React 实例,会导致 "Invalid hook call" 等运行时错误。
运行时原理
远程模块加载流程
Module Federation 的运行时加载是一个精心设计的异步过程:
关键 API
Module Federation 在运行时注入了两个关键的全局 API:
// 1. __webpack_init_sharing__:初始化共享作用域
// 在 Host 应用启动时调用,创建共享作用域
// 所有应用都会将自己的共享依赖注册到这个作用域中
await __webpack_init_sharing__('default');
// 2. __webpack_share_scopes__:共享作用域对象
// 存储所有已注册的共享依赖及其版本信息
console.log(__webpack_share_scopes__);
// {
// default: {
// react: {
// '18.2.0': {
// get: () => Promise<Module>, // 获取模块的工厂函数
// from: 'hostApp', // 来源应用
// eager: false, // 是否立即加载
// }
// }
// }
// }
容器接口
每个 Module Federation 应用生成的 remoteEntry.js 文件暴露了一个容器接口:
// remoteEntry.js 暴露的全局变量
interface Container {
// init: 初始化容器,接收共享作用域
init(shareScope: SharedScope): Promise<void>;
// get: 获取暴露的模块
get(modulePath: string): Promise<() => Module>;
}
// Host 加载远程模块的完整流程(伪代码)
async function loadRemoteModule(remoteName: string, modulePath: string): Promise<any> {
// 1. 确保共享作用域已初始化
await __webpack_init_sharing__('default');
// 2. 获取远程容器(通过 script 标签加载 remoteEntry.js 后挂载到 window 上)
const container = (window as any)[remoteName] as Container;
// 3. 初始化远程容器,传入共享作用域
await container.init(__webpack_share_scopes__.default);
// 4. 获取模块工厂函数
const factory = await container.get(modulePath);
// 5. 执行工厂函数,获取模块
const module = factory();
return module;
}
异步入口的必要性
使用 Module Federation 时,Host 应用的入口必须是异步的。这是因为在加载远程模块之前,需要先完成共享作用域的初始化和版本协商。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);
// 必须通过动态 import 加载真正的入口文件
import('./bootstrap');
如果直接在 index.ts 中同步执行入口逻辑,共享依赖可能还未初始化完成,导致运行时报错。
版本管理与协商机制
Module Federation 的共享依赖支持自动版本协商,避免多个应用加载不同版本造成冲突或冗余:
协商规则
版本协商策略
| 场景 | singleton | 行为 |
|---|---|---|
| 版本完全一致 | true/false | 复用同一份,无额外加载 |
| 版本兼容(满足 semver) | true | 使用最高兼容版本 |
| 版本不兼容 | true + strictVersion: false | 使用可用版本,打印 warning |
| 版本不兼容 | true + strictVersion: true | 抛出运行时错误 |
| 版本不同 | false | 各应用各自加载自己的版本 |
- 核心框架(React、Vue)必须
singleton: true,避免多实例问题 - 工具库(lodash、dayjs)可以不设
singleton,多版本共存通常无害 - 使用
requiredVersion明确版本范围,尽早发现不兼容问题 - 在 CI/CD 中检查各应用的依赖版本一致性
与微前端的关系
Module Federation 可以作为微前端的一种实现方案,但它与传统微前端方案(如 qiankun)有本质区别:
Module Federation vs qiankun 对比
| 对比维度 | Module Federation | qiankun |
|---|---|---|
| 本质 | 模块共享方案 | 微前端框架 |
| 粒度 | 模块级别(组件、函数) | 应用级别(整个子应用) |
| 技术栈 | 通常要求同构(Webpack 生态) | 技术栈无关 |
| JS 隔离 | 无隔离(同一全局作用域) | Proxy 沙箱隔离 |
| CSS 隔离 | 无内置隔离 | Shadow DOM / Scoped CSS |
| 共享依赖 | 内置共享机制,运行时协商 | 无内置共享,各应用独立打包 |
| 路由 | 无路由管理 | 基于路由的应用切换 |
| 通信 | 通过共享模块直接通信 | GlobalState / Props / CustomEvent |
| 构建工具 | 依赖 Webpack 5+(或兼容工具) | 与构建工具无关 |
| 部署 | 独立部署 | 独立部署 |
| 性能 | 更优(模块级按需加载,依赖复用) | 较重(应用级加载,可能重复依赖) |
| 适用场景 | 同技术栈多应用共享模块 | 异构技术栈集成 |
Module Federation 作为微前端方案的优缺点
优点:
- 模块级共享:粒度更细,可以只加载一个组件而非整个应用
- 依赖复用:内置的共享依赖机制避免了重复加载 React 等大型库
- 更好的性能:无需额外的沙箱层,无运行时开销
- 开发体验:远程模块使用方式与本地模块一致(
import) - 版本协商:自动处理共享依赖的版本冲突
缺点:
- 无隔离机制:没有 JS 沙箱和 CSS 隔离,全局变量可能冲突
- 构建工具耦合:强依赖 Webpack 5(或需要适配其他工具)
- 调试复杂:远程模块的错误堆栈、Source Map 调试较困难
- 版本管理:远程模块更新可能引发运行时兼容问题
- 无生命周期管理:不像 qiankun 有 mount/unmount 等生命周期钩子
- 如果多个应用使用相同技术栈且需要共享组件/工具,优先选择 Module Federation
- 如果需要集成不同技术栈的应用且需要强隔离,选择 qiankun 或无界
- 两者也可以结合使用:qiankun 管理应用级别的加载和隔离,Module Federation 处理模块级别的共享
实际应用场景
场景一:多应用共享组件库
多个独立应用共享同一个设计系统组件库,组件库更新后所有应用自动获取最新版本,无需重新构建:
import { container, Configuration } from 'webpack';
const config: Configuration = {
plugins: [
new container.ModuleFederationPlugin({
name: 'designSystem',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./Input': './src/components/Input',
'./Table': './src/components/Table',
'./Form': './src/components/Form',
'./theme': './src/theme/index',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
// 样式库也需要共享,确保主题一致
'styled-components': { singleton: true, requiredVersion: '^6.0.0' },
},
}),
],
};
场景二:运行时远程加载模块
根据用户权限或配置动态加载不同的功能模块:
interface RemoteConfig {
scope: string;
module: string;
url: string;
}
// 动态加载远程模块(运行时决定加载哪个远程应用)
async function loadDynamicRemote<T = any>(config: RemoteConfig): Promise<T> {
// 1. 动态注入 script 标签
await new Promise<void>((resolve, reject) => {
const script = document.createElement('script');
script.src = config.url;
script.type = 'text/javascript';
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error(`Failed to load ${config.url}`));
document.head.appendChild(script);
});
// 2. 初始化共享作用域
await __webpack_init_sharing__('default');
// 3. 初始化远程容器
const container = (window as any)[config.scope];
await container.init(__webpack_share_scopes__.default);
// 4. 获取模块
const factory = await container.get(config.module);
return factory() as T;
}
// 使用示例:根据后端配置动态加载
async function loadFeatureModules(): Promise<void> {
// 从 API 获取需要加载的远程模块配置
const response = await fetch('/api/feature-config');
const features: RemoteConfig[] = await response.json();
for (const feature of features) {
const module = await loadDynamicRemote(feature);
// 注册到应用的模块系统中
registerModule(feature.scope, module);
}
}
场景三:多团队协作的大型应用
电商平台由多个团队独立开发不同业务模块:
Module Federation 2.0
Webpack 团队推出的 Module Federation 2.0(也称为 Enhanced Module Federation)在 1.0 基础上进行了大幅增强:
主要改进
| 特性 | MF 1.0 | MF 2.0 |
|---|---|---|
| 类型提示 | 无,需手动维护 .d.ts | 自动生成和同步类型 |
| 运行时 API | 仅 Webpack 内置 | 独立的 @module-federation/runtime |
| 构建工具 | 仅 Webpack 5 | 支持 Webpack、Vite、Rspack 等 |
| 版本管理 | 基础 semver 协商 | 快照版本、版本回退、灰度发布 |
| 调试体验 | 较差 | Chrome DevTool 插件、Source Map 增强 |
| 动态远程 | 需要手动实现 | 内置动态远程支持 |
| 服务端渲染 | 有限支持 | 完善的 SSR 支持 |
MF 2.0 Runtime API 示例
import { init, loadRemote } from '@module-federation/runtime';
// 初始化 Module Federation 运行时
init({
name: 'hostApp',
remotes: [
{
name: 'remoteApp',
// 支持动态 URL,可以从配置中心获取
entry: 'http://localhost:3001/remoteEntry.js',
},
{
name: 'anotherApp',
entry: 'http://localhost:3002/remoteEntry.js',
},
],
shared: {
react: {
version: '18.2.0',
scope: 'default',
lib: () => require('react'),
shareConfig: {
singleton: true,
requiredVersion: '^18.0.0',
},
},
},
});
// 加载远程模块 —— 更简洁的 API
const RemoteButton = await loadRemote<React.FC<{ label: string }>>(
'remoteApp/Button'
);
Vite 中的 Module Federation
Vite 本身不内置 Module Federation 支持,但可以通过社区插件实现:
@originjs/vite-plugin-federation
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'remoteApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./utils': './src/utils/index',
},
shared: ['react', 'react-dom'],
}),
],
build: {
modulePreload: false,
target: 'esnext',
minify: false,
cssCodeSplit: false,
},
});
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
export default defineConfig({
plugins: [
react(),
federation({
name: 'hostApp',
remotes: {
remoteApp: 'http://localhost:5001/assets/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
});
- 开发模式下的兼容性不如 Webpack,部分场景需要
vite build --watch代替 dev server - 共享依赖的版本协商能力不如 Webpack 原生实现完善
- 生态和社区支持相对较少,生产环境使用需要充分测试
- 建议关注 Module Federation 2.0 的官方 Vite 支持(
@module-federation/vite)
常见面试问题
Q1: Module Federation 是什么?解决了什么问题?
答案:
Module Federation(模块联邦)是 Webpack 5 引入的一项核心特性,它允许多个独立构建、独立部署的应用在运行时动态共享 JavaScript 模块。
解决的核心问题:
- 跨应用代码共享:传统方式(npm 包发布)需要构建时安装依赖、应用重新打包部署。Module Federation 让应用在运行时直接加载另一个应用暴露的模块,实现即时共享
- 依赖重复加载:多个微前端子应用可能各自打包了一份 React、lodash 等公共库。Module Federation 的
shared机制让多个应用在运行时共享同一份依赖 - 独立部署的模块更新:组件库更新后,所有消费方应用自动获取最新版本,无需重新构建和部署
核心概念:
- Remote(提供者):通过
exposes暴露模块 - Host(消费者):通过
remotes声明并动态加载远程模块 - Shared:声明共享依赖,运行时自动版本协商,避免重复加载
// Remote 端:暴露模块
exposes: { './Button': './src/components/Button' }
// Host 端:消费模块(使用方式和本地模块一样)
const Button = React.lazy(() => import('remoteApp/Button'));
// Shared:共享 React,全局只加载一份
shared: { react: { singleton: true } }
Q2: Module Federation 的运行时原理是怎样的?(加载流程)
答案:
Module Federation 的运行时加载分为以下关键步骤:
第一步:初始化共享作用域
Host 应用启动时调用 __webpack_init_sharing__('default') 创建共享作用域(Shared Scope),将自身的共享依赖注册进去。
第二步:加载远程容器入口
当代码执行到 import('remoteApp/Button') 时,Webpack 运行时通过动态创建 <script> 标签加载远程应用的 remoteEntry.js 文件。该文件执行后会在 window 上挂载一个容器对象(如 window.remoteApp)。
第三步:初始化远程容器
调用 container.init(shareScope) 将共享作用域传递给远程容器。远程容器将自己的共享依赖也注册到同一个作用域中,并进行版本协商——选择满足所有 requiredVersion 约束的最高版本。
第四步:获取远程模块
调用 container.get('./Button') 获取指定模块的工厂函数,执行工厂函数得到模块实例。
// 1. 初始化共享作用域
await __webpack_init_sharing__('default');
// 2. 加载远程入口(script 标签)
await loadScript('http://localhost:3001/remoteEntry.js');
// 3. 初始化远程容器
const container = window.remoteApp;
await container.init(__webpack_share_scopes__.default);
// 4. 获取远程模块
const factory = await container.get('./Button');
const ButtonModule = factory();
强调两个关键设计:
- 异步入口:Host 入口必须通过
import('./bootstrap')异步加载,确保共享作用域在任何模块使用前完成初始化 - 共享作用域:所有应用共享同一个
shareScope对象,运行时根据singleton、requiredVersion等配置自动协商,决定是复用已有实例还是加载新版本
Q3: Module Federation 和微前端方案(如 qiankun)的区别?
答案:
两者的核心区别在于定位不同:
- Module Federation 是一个模块共享方案,解决的是"如何在运行时跨应用共享代码模块"
- qiankun 是一个微前端框架,解决的是"如何将多个独立应用集成为一个统一体验的大应用"
| 维度 | Module Federation | qiankun |
|---|---|---|
| 共享粒度 | 模块级(组件、工具函数) | 应用级(整个子应用) |
| JS 隔离 | 无隔离,共享全局作用域 | Proxy 沙箱,隔离全局变量 |
| CSS 隔离 | 无内置方案 | Shadow DOM / Scoped CSS |
| 依赖共享 | 内置 shared 配置,运行时协商 | 无内置机制,各子应用独立打包 |
| 技术栈 | 通常要求相同构建工具 | 完全技术栈无关 |
| 性能 | 更优,模块级按需加载 + 依赖复用 | 应用级加载,可能存在依赖冗余 |
| 生命周期 | 无(只是模块加载) | 完整的 mount/unmount/bootstrap |
选择建议:
function chooseArchitecture(project: ProjectInfo): string {
// 场景 1:同技术栈 + 共享组件
if (project.sameTechStack && project.needShareModules) {
return 'Module Federation';
}
// 场景 2:异构技术栈 + 强隔离需求
if (project.multiTechStack && project.needIsolation) {
return 'qiankun / wujie';
}
// 场景 3:两者结合
if (project.complexEnterprise) {
return 'qiankun(应用管理) + Module Federation(模块共享)';
}
return '评估具体需求后决定';
}
提到两者可以结合使用:用 qiankun 管理应用级别的加载、路由和隔离,用 Module Federation 在子应用之间共享公共组件库和工具模块。这种组合既有强隔离能力,又有高效的代码共享。