Webpack 5 带来了许多重量级更新,例如持久化缓存、更好的 Tree Shaking 以及 Top Level Await 支持,但其中对工程架构影响最深远的,当属 Module Federation,也就是模块联邦。这套机制背后的设计理念常被社区称为 Equality 平等:联邦内的每一个构建产物没有绝对的主从关系,任何应用都可以既是模块的生产者,又是模块的消费者。本文将从这一理念出发,深入分析模块联邦的工作原理与实践方式。

一、什么是 Equality 平等理念
在传统的多应用架构中,代码共享往往依赖 npm 包发布或者 externals 外链脚本。这两种方式都隐含了一个不平等的前提:存在一个中心化的依赖提供方,其他应用必须被动地按约定引入。npm 方式要求所有应用同步升级版本,externals 方式则要求全局变量命名严格统一,任何一个环节出错都会导致运行时冲突。
模块联邦打破了这种中心化的不平等关系。每个参与联邦的应用(在 webpack 中称为一个 Container,容器)都可以通过 exposes 配置向外暴露自己的某些模块,同时通过 remotes 声明自己想要消费的其他容器。A 应用可以引用 B 应用的按钮组件,B 应用也可以反过来引用 A 应用的工具函数,双方地位完全对等。这种双向、去中心化的模块共享能力,正是 Equality 平等一词的核心含义。
更重要的是,这种共享发生在运行时而非构建时。消费方并不需要把生产方的代码打包进自己的 bundle,而是通过异步加载远程脚本的方式动态获取,这让独立部署、独立发布的微前端架构成为可能。
二、Module Federation 核心配置剖析
要理解联邦机制,最直接的方式是看一份真实配置。假设我们有两个应用:app1 作为消费方(host),app2 作为提供方(remote),下面是 app2 的核心配置。
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app2',
filename: 'remoteEntry.js',
exposes: {
// 对外暴露一个按钮组件,消费方通过 app2/Button 引用
'./Button': './src/components/Button.js',
},
shared: {
// 声明共享依赖,避免多个应用各自打包一份 React
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
},
}),
],
};这段配置中有三个关键点。name 是容器的全局唯一标识;filename 指定入口文件名,消费方将加载这个文件来获取容器的模块清单;exposes 则是一个映射表,键是外部访问路径,值是本地模块的真实路径。只有被 exposes 显式暴露的模块才会进入容器清单,未暴露的内部实现对外完全不可见,这保证了模块的封装性。
接下来看消费方 app1 的配置,它通过 remotes 声明对 app2 的引用。
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
remotes: {
// 远程容器的地址,指向 app2 部署后的 remoteEntry.js
app2: 'app2@https://cdn.ipipp.com/app2/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true },
},
}),
],
};在业务代码里,消费方可以直接用异步引入的方式使用远程组件,体验与本地懒加载完全一致。
const RemoteButton = React.lazy(() => import('app2/Button'));
function App() {
return (
<React.Suspense fallback={<div>加载中...</div>}>
<RemoteButton />
</React.Suspense>
);
}三、Shared 依赖共享与版本协商机制
shared 配置是联邦机制中最精巧的部分,也是 Equality 理念最直接的体现。当多个应用都依赖 React 时,如果各自打包一份,页面上会出现多份 React 实例,导致 hooks 状态错乱、上下文丢失等严重问题。shared 机制允许容器之间协商复用同一份依赖。
协商的过程大致如下:应用启动时,webpack 的运行时代码会检查全局共享作用域中是否已经存在符合版本要求的 React。如果 host 已经加载了 18.2.0 版本,而 remote 要求的版本范围是 ^18.0.0,那么 remote 会直接复用 host 的副本,跳过自己的下载。singleton 设为 true 则更进一步,强制全局只保留一个实例,即使版本不完全匹配也优先复用,这对 React 这类有全局状态的库几乎是必选项。
需要注意的是,异步加载共享依赖会导致模块初始化变成异步过程。因此使用 shared 的应用必须提供一个异步入口点,例如 bootstrap.js 模式。
// index.js 同步入口,只做异步引导
import('./bootstrap');
// bootstrap.js 真正的应用初始化逻辑
import React from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')).render(<App />);这种写法看起来多了一层间接,但它保证了在应用代码执行之前,所有共享依赖都已完成协商与加载,避免了竞态问题。如果不采用异步边界,构建时 webpack 会给出警告,运行时可能出现依赖版本不一致的隐蔽错误。
四、与传统共享方案的对比与落地建议
对比三种常见方案可以更清楚地看到联邦的价值。npm 私有包共享需要经历发布、安装、升级的完整流程,任何一次改动都要全量更新,交付链路长;externals 配合 CDN 脚本虽然也能避免重复打包,但缺少版本协商能力,多应用共存时极易产生全局变量冲突;模块联邦则在运行时完成了依赖发现、版本匹配与按需加载三件事,灵活性和安全性都明显更高。
在落地微前端项目时,有几点实践建议值得参考。第一,shared 中对 React、Vue 这类框架库务必开启 singleton,否则多实例问题几乎必然出现。第二,remote 入口地址建议通过环境变量注入,方便区分测试与生产环境的容器地址。第三,团队间应约定 exposes 的路径规范,例如统一以组件类型为前缀,避免消费方引用路径混乱。第四,要为远程加载失败设计降级方案,例如用 ErrorBoundary 捕获懒加载异常并渲染兜底 UI,防止单个容器故障拖垮整个页面。
总而言之,Webpack 5 的模块联邦通过运行时协商与去中心化的容器设计,真正实现了应用之间平等共享代码的目标。它既不是简单的动态脚本注入,也不是传统的包管理,而是一套介于两者之间的模块分发体系。理解了 Equality 平等这一设计哲学,再回看 exposes、remotes、shared 三个配置项,你会发现它们的协作方式都变得顺理成章,这正是掌握模块联邦的关键所在。
Webpack 5模块联邦Module Federation修改时间:2026-08-31 23:01:10