跳到主要内容

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 插件实现。

核心配置字段

字段类型说明
namestring容器名称,全局唯一,作为全局变量暴露
filenamestring容器入口文件名,通常为 remoteEntry.js
exposesobject暴露给其他应用使用的模块映射
remotesobject声明要消费的远程应用及其入口
sharedobject | array声明需要共享的依赖及其策略

Remote 端配置(提供者)

remote-app/webpack.config.ts
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 端配置(消费者)

host-app/webpack.config.ts
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 中使用远程模块

host-app/src/App.tsx
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 插件自动生成:

host-app/src/types/remoteApp.d.ts
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 最关键的配置之一,它决定了多个应用之间如何复用公共依赖:

shared 配置示例
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',
};
配置项类型默认值说明
singletonbooleanfalse是否为单例,确保全局只有一份实例
requiredVersionstringpackage.json 中的版本要求的语义化版本范围
eagerbooleanfalse是否立即加载而非异步
strictVersionbooleanfalse版本不匹配时是否报错(否则仅 warning)
shareKeystring包名在共享作用域中的标识
shareScopestring'default'共享作用域名称
singleton 的重要性

像 React 这样的库必须设置 singleton: true,因为 React 内部依赖全局状态(如 Hooks 链表)。如果页面上存在两个不同的 React 实例,会导致 "Invalid hook call" 等运行时错误。

运行时原理

远程模块加载流程

Module Federation 的运行时加载是一个精心设计的异步过程:

关键 API

Module Federation 在运行时注入了两个关键的全局 API:

运行时关键 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 应用的入口必须是异步的。这是因为在加载远程模块之前,需要先完成共享作用域的初始化和版本协商。

bootstrap.ts — 真正的入口
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(<App />);
index.ts — 异步加载入口
// 必须通过动态 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 Federationqiankun
本质模块共享方案微前端框架
粒度模块级别(组件、函数)应用级别(整个子应用)
技术栈通常要求同构(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 处理模块级别的共享

实际应用场景

场景一:多应用共享组件库

多个独立应用共享同一个设计系统组件库,组件库更新后所有应用自动获取最新版本,无需重新构建:

design-system/webpack.config.ts
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' },
},
}),
],
};

场景二:运行时远程加载模块

根据用户权限或配置动态加载不同的功能模块:

dynamic-remote-loader.ts
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.0MF 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 示例

使用 @module-federation/runtime
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

vite.config.ts(Remote 端)
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,
},
});
vite.config.ts(Host 端)
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'],
}),
],
});
Vite Module Federation 的局限性
  • 开发模式下的兼容性不如 Webpack,部分场景需要 vite build --watch 代替 dev server
  • 共享依赖的版本协商能力不如 Webpack 原生实现完善
  • 生态和社区支持相对较少,生产环境使用需要充分测试
  • 建议关注 Module Federation 2.0 的官方 Vite 支持(@module-federation/vite

常见面试问题

Q1: Module Federation 是什么?解决了什么问题?

答案

Module Federation(模块联邦)是 Webpack 5 引入的一项核心特性,它允许多个独立构建、独立部署的应用在运行时动态共享 JavaScript 模块。

解决的核心问题:

  1. 跨应用代码共享:传统方式(npm 包发布)需要构建时安装依赖、应用重新打包部署。Module Federation 让应用在运行时直接加载另一个应用暴露的模块,实现即时共享
  2. 依赖重复加载:多个微前端子应用可能各自打包了一份 React、lodash 等公共库。Module Federation 的 shared 机制让多个应用在运行时共享同一份依赖
  3. 独立部署的模块更新:组件库更新后,所有消费方应用自动获取最新版本,无需重新构建和部署

核心概念:

  • 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();
面试加分点

强调两个关键设计:

  1. 异步入口:Host 入口必须通过 import('./bootstrap') 异步加载,确保共享作用域在任何模块使用前完成初始化
  2. 共享作用域:所有应用共享同一个 shareScope 对象,运行时根据 singletonrequiredVersion 等配置自动协商,决定是复用已有实例还是加载新版本

Q3: Module Federation 和微前端方案(如 qiankun)的区别?

答案

两者的核心区别在于定位不同

  • Module Federation 是一个模块共享方案,解决的是"如何在运行时跨应用共享代码模块"
  • qiankun 是一个微前端框架,解决的是"如何将多个独立应用集成为一个统一体验的大应用"
维度Module Federationqiankun
共享粒度模块级(组件、工具函数)应用级(整个子应用)
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 在子应用之间共享公共组件库和工具模块。这种组合既有强隔离能力,又有高效的代码共享。

相关链接