Webpack 5 模块联邦(Module Federation)学习
什么是模块联邦(Module Federation)? 模块联邦是 Webpack 5 引入的一项革命性功能,它允许多个独立构建的前端模块在运行时组合成单一的应用程序。简单来说,每个独立构建(可以视为一个子应用)可以像「容器」一样暴露自身的一些模块(如组件、函数等),供其它构建动态加载和使用。这使得不同应用之间可以直接共享代码,而无需将代码发布到NPM再安装引用。从运行时看,多个构建的模块组成了一个巨大的连接模块图,彼此之间像本地模块一样协作。这一特性打破了以往前端应用相互独立的壁垒,为实现微前端架构提供了官方支持。

1. 基础概念
什么是模块联邦(Module Federation)? 模块联邦是 Webpack 5 引入的一项革命性功能,它允许多个独立构建的前端模块在运行时组合成单一的应用程序。简单来说,每个独立构建(可以视为一个子应用)可以像「容器」一样暴露自身的一些模块(如组件、函数等),供其它构建动态加载和使用。这使得不同应用之间可以直接共享代码,而无需将代码发布到NPM再安装引用。从运行时看,多个构建的模块组成了一个巨大的连接模块图,彼此之间像本地模块一样协作。这一特性打破了以往前端应用相互独立的壁垒,为实现微前端架构提供了官方支持。

Webpack 5 为什么引入模块联邦? 随着前端应用日益庞大和团队协作需求增加,传统的单体架构难以满足独立部署和扩展的需求。Webpack 5 引入模块联邦正是为了解决跨应用共享代码的痛点在模块联邦之前,常用的代码共享方式包括将通用模块发布为私有NPM包、使用iframe
嵌入独立应用,或利用 Webpack 的 DLL/Externals 等方案。然而这些方法要么增加了构建发布成本,要么在不同项目间无法做到按需“热插拔”。模块联邦通过运行时直接加载远程模块,使得应用之间共享代码如同加载本地模块一样简便,大幅降低了团队协作发布的耦合度和等待时间。例如,一个团队发布了新功能组件,另一个团队的应用可以在不重新构建自身的情况下直接加载使用,实现了真正的模块即服务。
适用的场景: 模块联邦最典型的应用场景是微前端架构。当一个大型前端项目拆分为多个独立团队开发的模块时(例如按业务域拆分为商品、购物车、用户等子应用),模块联邦可以让这些子应用独立部署,又能在运行时集成到一起,构成统一的界面。除此之外,还有一些场景非常适合模块联邦:
- 跨应用代码共享: 如多个产品线Web应用需要共享统一的设计系统或组件库,使用模块联邦可以将该组件库作为远程模块暴露,所有应用在运行时加载最新版本的组件,实现设计系统的实时同步更新。
- A/B测试和插件化加载: 可以将某些功能做成独立的远程模块,在特定情况下按需加载。例如进行A/B测试时,通过模块联邦动态加载不同版本的模块以测试用户反馈,或提供插件机制让应用在运行时加载第三方扩展模块。
- 多团队并行开发: 不同团队负责不同模块,各自独立发布,最终由壳(Host)应用整合。这种模式下,模块联邦提供了团队之间清晰的边界,各模块只通过约定的接口交互,团队可以在不影响其它应用的前提下自由演进各自的代码。
- 渐进重构大型应用: 将已有的单体应用逐步拆分为微前端模块时,可利用模块联邦在旧应用中动态加载新模块。这样可以逐步替换旧功能,而不需要一次性重构或切换,大大降低重构风险(这一点将在后文实际案例中详细讨论)。
2. 环境搭建与配置
Webpack 5 Module Federation 插件概述: Webpack 5 内置了对模块联邦的支持,主要由一个名为ModuleFederationPlugin
的插件来实现配置。它是一个高级插件(high level plugin),包装了底层的容器相关功能,使我们可以通过简单的配置接口定义哪些模块要暴露(exposes)、要消费哪些远程模块(remotes)、以及如何处理共享依赖(shared)等。值得注意的是,Module Federation 插件随 Webpack 附带,无需额外安装,只需从 webpack 库中引入即可使用(例如:const { ModuleFederationPlugin } = require('webpack').container;
)。
启用 Module Federation 的基本步骤如下:
- 安装/升级 Webpack 5: 确保项目使用的是 Webpack 5 及以上版本,因为模块联邦是 Webpack 5 引入的新特性。如果是已有项目升级,注意也升级 webpack-cli、dev-server 等相关工具以获得对 Webpack 5 的兼容支持。
- 引入 ModuleFederationPlugin: 在 webpack 配置的
plugins
数组中使用new ModuleFederationPlugin({...})
进行配置。典型情况下,需要在**宿主应用(Host)和远程应用(Remote)**各自的 webpack 配置中都添加此插件,并按各自角色配置不同选项。
Remote 和 Host 的概念及配置方式: Module Federation 中有两个核心角色:远程模块提供方(Remote)和宿主应用(Host)。Remote 暴露模块,Host 消费模块。一个应用也可以同时扮演 Host 和 Remote(既暴露给别人又消费别人的模块),但基础原理可以先按单向依赖理解。
- Remote(远程应用)配置: 在远程应用的 webpack 配置中,使用 ModuleFederationPlugin 时需要重点设置
name
、filename
和exposes
。例如,有一个远程应用名为remoteApp
,要暴露一个名为MyWidget
的组件模块:
// remote 应用 webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
// ...其它配置如 entry/output等
plugins: [
new ModuleFederationPlugin({
name: "remoteApp", // 远程应用的名称
filename: "remoteEntry.js", // 对外暴露的文件名,Webpack将生成这个文件
exposes: {
"./MyWidget": "./src/components/MyWidget" // 映射:提供给外部的模块名 -> 本地模块路径
},
shared: {
/* 共享依赖配置(可选),示例见下文共享依赖章节 */
}
})
]
}
- 上述配置会使 Webpack 构建出一个
remoteEntry.js
文件,其中包含了MyWidget
组件模块的代码包装(作为远程可加载模块)。name
用于标识该远程,在其他应用引用它时需要用到;exposes
定义了可以公开的模块键值对。在构建时,Webpack会把MyWidget
相关的代码拆分到remoteEntry.js
,这样它可以独立于远程应用的其他部分被加载。 - Host(宿主应用)配置: 宿主应用需要配置
remotes
字段,指明将使用哪些远程模块以及它们如何访问。还是以上述remoteApp
为例,宿主应用想要使用该远程提供的MyWidget
:
// 宿主应用 webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
// ...entry/output 等
plugins: [
new ModuleFederationPlugin({
name: "hostApp", // 宿主应用名称(自身也可作为 remote 名称暴露给别的应用,如果需要)
remotes: {
// 键为在代码中引用远程时使用的标识,值为远程的名称@远程URL
remoteApp: "remoteApp@http://localhost:3001/remoteEntry.js"
},
shared: {
/* 共享依赖配置 */
}
})
]
}
在remotes
中,remoteApp
左侧键名表示在宿主代码中将通过import("remoteApp/模块")
来引用这个远程,右侧字符串remoteApp@.../remoteEntry.js
的格式则告诉 webpack:远程应用实际运行时的全局名称是remoteApp
,它的远程入口文件地址在哪儿(这里假设远程部署在localhost:3001)。当宿主应用运行时,若代码中遇到对remoteApp/...
的import,Webpack的运行时将根据此配置去加载远程的入口脚本remoteEntry.js
。
动态加载远程模块的方式: 配置好以上 Module Federation 参数后,在实际代码中就可以通过动态导入来加载远程模块。例如,在宿主应用的业务代码中:
// 在 React 中动态加载远程组件
import React, { Suspense, lazy } from 'react';
const RemoteWidget = lazy(() => import("remoteApp/MyWidget"));
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<RemoteWidget /> {/* 像使用本地组件一样使用远程组件 */}
</Suspense>
);
}
通过 React 的lazy
和Suspense
,可以实现对远程组件的懒加载和加载指示。对于非 React 项目,也可以直接使用动态import()
语法:
// 非React环境下直接动态导入
async function loadRemoteWidget() {
const remoteModule = await import("remoteApp/MyWidget");
// remoteModule 即为远程导出的模块对象,可从中获取组件或函数
remoteModule.renderWidget();
}
关键在于引用语法"remoteApp/MyWidget"
,Webpack 会将其识别为远程模块调用。在运行时首次执行到这行时,宿主会按之前remotes
配置,生成一个<script>
标签去加载http://localhost:3001/remoteEntry.js
。一旦远程入口脚本加载完毕,Webpack runtime 会异步获取其中暴露的MyWidget
模块代码,然后再继续执行后续逻辑。由于这一过程是异步的,所以使用时通常需要配合await
或类似上面 React Suspense 的方式处理。值得一提:Webpack 5 Module Federation 甚至支持在运行时动态地添加远程模块配置(即应用启动时并不知道可能的远程有哪些,通过运行时提供远程的URL再加载)。这种高级用法利用了 container 的init
接口,可以在应用运行过程中注册新的远程容器并加载其模块。一般情况下,静态配置已能满足需求,但对于插件体系等场景,动态加载新的 remote 也非常强大。

3. 共享依赖管理
在多应用组合的场景下,**共享依赖(shared dependencies)**的处理至关重要。如果宿主和远程应用各自打包了相同的第三方库(例如 React、lodash 等),则用户在加载页面时可能会因为重复加载相同库而浪费带宽,甚至因为版本不一致导致冲突。模块联邦通过shared
配置和运行时的共享机制,优雅地解决了这一问题。
共享依赖的配置: 在 ModuleFederationPlugin 的配置中,可以使用shared
字段声明哪些依赖是可以共享的。示例:
new ModuleFederationPlugin({
name: "remoteApp",
exposes: { ... },
remotes: { ... },
shared: {
react: { singleton: true, requiredVersion: "^18.2.0" },
"react-dom": { singleton: true, requiredVersion: "^18.2.0" },
lodash: { singleton: false }
}
})
以上配置表明:react
和react-dom
将被视为单例依赖(singleton),希望运行时确保无论Host或Remote,加载的都是同一个版本18.2.x的 React 实例;lodash
也声明为共享,但非单例——这意味着如果版本不匹配,允许各自加载各自的版本。requiredVersion
指定了所需的版本范围,当版本不符时,Webpack 会在控制台给出警告或错误,帮助开发者发现版本冲突。
避免多个版本依赖加载的问题: 通过合理使用shared
配置,可以避免重复加载。例如,当Host应用已经加载了React 18.2.0,远程也需要React且声明了相同版本的共享,则远程在初始化时会直接复用Host已加载的React实例,不会再去请求React库代码。这不仅减少了网络请求和资源大小,也保证了远程组件和宿主组件使用的是同一个React上下文,避免因为不同实例导致的潜在问题(如两个不同React实例管理各自state,会造成无法一致更新UI等)。如果出现版本不一致的情况,比如Host是React17而Remote要求React18,默认情况下Webpack会尝试各自加载各自的版本,但我们可以通过设置strictVersion: true
来强制版本匹配,一旦不匹配就抛出错误。这种严格模式可以及时发现版本冲突,防止问题默默发生。
Webpack 对共享模块采取**「优先使用已加载模块」**的策略:Host 启动时会把自身打包内标记为shared
的库加入共享池(Share Scope),当Remote加载时,会先检查共享池中是否已有可用版本的该库。如果有且版本满足要求,Remote就直接使用宿主提供的模块;如果没有,Remote才会下载自己的版本并加载。此外,Module Federation还有机制支持多个不同版本并存(例如在嵌套微前端场景下),但一般应尽量避免,将依赖版本统一或兼容。
运行时动态共享依赖: 共享机制是在运行时动态完成的。当宿主加载远程容器时,会调用远程容器暴露的 init
方法,将宿主的共享依赖映射传递过去。这一步允许远程知道宿主已经有哪些库可用,从而选择使用还是忽略自己的。这种动态协商确保了即使各应用独立部署,也能在用户侧协同工作。需要注意,若双方版本不一致且没有strictVersion,Webpack默认会输出警告,并优先使用宿主的版本(忽略远程提供的版本),以避免页面加载两个不同版本的同一库。为了减少冲突风险,团队间应就核心依赖(如框架版本)提前达成一致,或采用兼容层策略。
4. 模块联邦的实现原理
模块联邦之所以能够在运行时联结不同构建的模块,背后依赖于 Webpack 构建期和运行时的一系列协同机制。理解其实现原理有助于我们更好地调优和排查问题。
Webpack 运行时如何管理远程模块: 当使用 Module Federation 时,Webpack 在构建输出中会包含一个特殊的**容器(runtime container)**用于管理远程模块。对每个 Remote 应用,Webpack 会生成一个远程容器入口文件(即前述remoteEntry.js
),它本质上是一个自注册的模块容器,包含两个重要的方法:get(module)
用于按需获取远程暴露的模块,init(shareScope)
用于接受共享依赖。宿主在需要加载远程模块时,Webpack runtime 会执行如下步骤:
- 加载远程容器脚本: 首次遇到对某个远程的调用时(例如第一次
import("remoteApp/Module")
),宿主应用的Webpack运行时通过动态插入<script src="http://remoteAppHost/remoteEntry.js">
来拉取远程容器。这个过程与正常的按需加载(chunk loading)类似,只不过加载的是远程的入口文件。 - 初始化共享作用域: 一旦远程脚本加载完毕,宿主会调用远程容器的
init(__webpack_share_scopes__.default)
方法,将宿主这边的共享依赖信息传递给远程。这样远程就知道哪些依赖已经有可用实例,无需再加载。 - 请求模块并执行: 完成init后,宿主调用远程容器的
get('Module')
获取模块工厂函数,然后调用该工厂函数执行,得到真正的模块导出。这个模块对象就和本地模块一样使用了。上述流程都是Webpack在幕后自动完成的,开发者只需使用动态import语法即可。
需要特别指出,Webpack确保了远程模块的加载(异步)和执行(同步)分离:即远程脚本是异步获取的,但一旦获取完毕,和本地模块一起按正确的依赖顺序执行,从而不会因为模块来源不同而扰乱应用的执行顺序。

代码拆分与动态加载: 模块联邦充分利用了 Webpack 的代码拆分能力。远程应用通过 ModuleFederationPlugin 把要暴露的模块拆分成独立的 bundle(如前述的 remoteEntry.js),使其可以按需加载。而宿主对于远程模块的引用被Webpack视为一种特殊的外部请求,不会打包进主应用,而是在运行时动态获取。这意味着只有当用户真正需要某个远程模块时,相关代码才会被下载,避免了初始加载过大的问题。开发者也可以结合应用路由进行优化:例如在单页应用(SPA)中,主应用可以在路由切换到需要某远程模块的页面时,再触发加载,从而实现与路由懒加载相结合的效果。
模块联邦插件的内部机制: ModuleFederationPlugin 实际上是在构建时做了两方面工作:
- 对于远程应用,它使用内部的 ContainerPlugin,为每个
exposes
的模块创建一个容器入口和对应的异步加载设置。Webpack 会据此输出远程容器bundle,并在其中注册提供的模块。 - 对于宿主应用,它使用 ContainerReferencePlugin,将指定的远程定义为一种特殊的外部模块。当遇到对远程模块的import时,这个插件让Webpack不要试图打包该模块,而是生成运行时代码去加载远程容器。它还负责注入共享依赖的处理逻辑(override机制),确保宿主提供的共享模块能被远程优先使用。
通过 ModuleFederationPlugin 的高层封装,开发者无需手动设置 externals 或脚本加载逻辑,就可以把多个构建产物编织在一起。Webpack 会在输出的bundle中包含必要的元数据描述,比如远程模块清单、版本要求,以及用于协商共享依赖的代码。这些数据在应用启动和远程加载时发挥作用,使不同来源的模块能够在同一运行环境中共存。
从实现上看,模块联邦让各个独立构建产物之间建立了一种松耦合的连接:它们各自独立编译、部署,但通过约定的接口在浏览器运行时互相发现对方并合作。这种设计带来了极大的灵活性,但也要求我们在使用时遵循约定,如远程和宿主的名称、URL需对应,接口契约需稳定等。
5. 最佳实践
模块联邦提供了新的可能性,但要充分发挥其优势,在项目组织和协作上需要遵循一些最佳实践。
项目结构组织: 对于采用模块联邦的项目,可以选择独立仓库或单一仓库(monorepo)的方式来管理。无论哪种方式,建议确保每个 Remote 应用有清晰的边界和入口。下面是两种策略的对比:
- 独立代码库: 每个微前端应用(Remote 或 Host)各自一个代码仓库,独立构建和部署。优点是团队边界清晰,完全独立的发布周期;缺点是在本地开发和集成测试时需要协调启动多个项目,且共享的类型定义或工具库需要通过npm包等方式同步。
- Monorepo 单仓库: 将多个模块放在一个仓库下(可以使用例如 Nx、Lerna 等工具)。优点是依赖共享和一致性更容易,例如可以共享lint规则、类型接口,甚至直接引用源码避免重复;同时本地运行微前端架构更方便(通过统一的脚本启动多个服务)。缺点是仓库规模大,CI流程复杂度提高,各团队在版本控制上可能需要更严格约定避免相互影响。
在组织代码时,一个常见的模式是让宿主应用承担路由和框架加载的职责,而各 Remote 提供具体业务模块的界面和逻辑。例如,可以有一个Shell仓库负责整体应用的框架壳(导航栏、路由配置、用户认证等),不同团队的Remote仓库负责各自业务功能。当需要支持双向联邦(既是Host又是Remote)时,需确保避免循环依赖的复杂性,通常通过定义明确的依赖方向或公共通信机制(如全局事件、shared状态库)来避免死锁。
性能优化与减少不必要请求:
- 按需加载远程:尽可能延迟加载远程模块。在路由切换或用户确实需要某功能时再加载对应 remote 的代码,而不是应用初始化时一次性加载所有 remote。比如首屏只加载核心Host和某些常用Remote,其它 Remote 等相应页面首次访问时再加载。这样可以显著减少初始加载体积和请求数。
- 预加载与缓存:对于用户在一次会话中很可能会用到的远程模块,可以考虑在闲时预获取远程入口。比如利用
<link rel="prefetch">
提示浏览器提前下载某些 remoteEntry.js,提高稍后点击时的响应速度。同时要确保静态资源开启了HTTP缓存策略,远程模块不每次都重复下载。远程的文件名中可带版本或hash,以便内容更新时客户端能获取新版本,否则走缓存。 - 公共依赖的提取:利用共享依赖机制,确保大的基础库(如框架、组件库)不被重复打包。Host 应尽量提供这些共享库,Remote 标记为shared并使用
singleton
,这样只加载一份。对于体积较大的共享库,也可以考虑使用CDN外链(externals)方式,让所有应用引用同一URL的脚本,实现缓存最大化——但这需要放弃模块联邦对该库的版本协商能力,需自行保证版本一致。 - 减少开销和隔离影响:留意远程模块的初始化时间和副作用。如果某远程模块在加载时会执行大量初始化逻辑(例如大量请求或全局状态修改),可能拖慢页面切换或引入冲突。尽量将模块设计为纯组件,在被实际渲染时才执行必要操作。此外,考虑网络失败的情况,如远程加载失败的超时处理,确保应用有降级UI或重试机制,避免整块功能空白。
版本管理策略: Host 与 Remote 之间的版本兼容性至关重要,特别是在各模块独立部署、可能不同步发布的情况下。以下是几种版本管理策略及其权衡:
版本管理策略 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
依赖版本统一(尽量同步升级) | - 各模块间冲突少,运行时稳定 - 共享效率高,仅加载一份依赖 | - 团队需协调依赖升级节奏,可能拖慢独立发布 - 降低了团队技术栈多样性的自由度 | 核心框架和库较少变动的项目 团队沟通良好,能同步规划升级 |
严格单一版本(strictVersion 单例) | - 确保运行时只存在单一版本依赖,杜绝隐性冲突 - 不匹配立即报错,问题早暴露 | - 版本不匹配时会阻止功能加载,需要紧急修复或协调 - 对版本要求极为严格,可能不利于渐进升级 | 对稳定性要求极高的模块 如核心UI库、框架本身 |
多版本并行(路由区分版本部署) | - 可逐步升级:新版本Remote与旧版同时存在,不影响旧功能 - Host可针对不同URL加载不同版本Remote,实现灰度发布 | - 部署和运维复杂度提升,需要维护多个版本实例 - Host需要逻辑选择加载哪个版本,增加代码复杂性 | 大型应用渐进升级、大版本改版 需要灰度发布或A/B测试 |
兼容适配层 | - Remote做向下兼容处理,确保对旧Host也能工作 - Host对新Remote的返回做适配,减少同步发布需求 | - 增加开发工作量,实现和测试成本高 - 只适用于特定模块,无法解决底层框架不兼容 | 少量模块发生Breaking Change 且有足够资源做双层兼容 |
通常,将核心共享依赖锁定在相同的主版本是最简洁可靠的做法。例如Host和各Remote都使用React 18.x,同步升级次要版本。这需要团队在规划时协商一致。此外,建议在CI中加入跨仓库的集成测试,在Remote发布新版本时运行一遍与Host集成的测试用例,及时发现不兼容。
对于需要长期并存的不同版本,可采用不同remote名称或URL区分(如remote_v1
和remote_v2
),Host 根据情况调用不同版本,以换取演进的灵活性。
CI/CD 与持续部署: 模块联邦的引入会影响持续集成和部署流程:
- 各Remote可以有独立的CI流水线,在通过测试后自动部署到静态服务器或CDN。部署完成后,无需重启或重新部署 Host,用户刷新页面即可加载到新版本远程模块。这实现了真正的独立部署。但前提是保持远程接口向后兼容,否则新Remote上线可能破坏旧Host中的功能。
- Host的部署相对减少频率,可能仅在路由调整、新增Remote引用或重大框架升级时才需要发布。这种情况下Host更像一个稳定的壳,托管各种版本兼容的Remote。
- 为确保质量,CI 中应增加跨应用的集成测试环节。例如在Host的仓库中,配置一个测试流程:拉取最新的若干 Remote 构建产物(或使用发布环境的URL),运行端到端测试,验证整体应用功能。这可以捕获由于Remote更新导致的集成问题,防止不兼容代码进入生产。
- 如果使用单一仓库(monorepo),可以借助工具按模块变化触发有选择的构建部署;如果独立仓库,则需通过约定版本或通知机制让Host感知Remote的新版本是否兼容(例如Remote发布时在Git或注册表打tag,Host定期同步依赖声明)。
- 持续部署还需要考虑缓存失效策略:当Remote发布新版本,如果仍复用相同的remoteEntry.js URL,浏览器可能缓存旧文件。因此通常Remote的文件名或路径中会包含版本号或hash,例如
remoteEntry.1.2.0.js
,Host改用新URL去加载,以确保用户获得更新rangle.io。也可以在文件名不变时,通过HTTP头禁止长缓存或采用Service Worker策略控制更新。
团队协作与治理: 在模块联邦架构下,不同团队负责不同模块,协作时应达成一些共识:
- 明确接口契约: Remote 模块暴露的接口(组件属性、调用方式等)应文档化并保持相对稳定。任何 Breaking Change 都需提前沟通,并通过兼容层或版本升级策略平滑过渡。
- 代码审查交叉参与: 可以考虑让使用某Remote的Host团队成员参与该Remote仓库的PR评审,反之亦然rangle.io。这样能及时让消费方了解Remote的变化,并给提供方反馈可能的影响,减少集成问题。
- 共享工具和标准: 虽然团队独立,但可以共享一些 lint 规则、测试框架、版本发布规范,保证基本的代码质量一致性。同时,约定统一的依赖版本范围,避免有人升级某库造成其他模块不兼容的情况。
- 故障隔离: 建议Host对Remote的加载失败做好容错处理,例如加载超时或报错时,给予用户提示或采取备用方案(比如通知用户某模块暂不可用)。在极端情况下,如果某Remote部署失误,可以临时在Host端屏蔽对它的调用或回退到备用版本(如果有)。总之,要避免单个Remote故障蔓延影响整个应用。
- 权限和安全: 如果多个Remote托管在不同团队甚至不同子域下,注意处理好认证授权问题。例如用户登录态(JWT token等)在不同来源之间的共享,CORS 策略的配置,等等。通常可以把身份认证放在Host统一处理,生成授权信息传递给Remote,或者让Remote通过共享的认证模块获取用户态。
6. 实战案例
本章节通过实际案例,展示如何从零搭建微前端架构,以及模块联邦在流行框架和企业项目中的应用方式。
案例1:从零搭建微前端项目
设想一个简单场景:我们要将“产品列表”和“购物车”两个模块拆分为独立的前端应用,由不同团队开发,并最终由主应用整合。这两个模块分别作为 Remote 应用暴露出 React 组件,主应用作为 Host 通过路由加载它们。
步骤概要:
- 初始化项目结构: 创建三个独立的前端项目:
shell-app
(主应用)、products-app
(产品列表微前端)、cart-app
(购物车微前端)。每个都使用 React + Webpack5 脚手架(如 Create React App 自定义配置或 Vite 等,确保使用 Webpack5)。 - 配置模块联邦: 在
products-app
的 webpack 配置中,使用 ModuleFederationPlugin 暴露产品列表组件:
// products-app 的 webpack.config.js
new ModuleFederationPlugin({
name: "productsApp",
filename: "remoteEntry.js",
exposes: { "./ProductsPage": "./src/ProductsPage" },
shared: { react: { singleton: true, requiredVersion: "^18.0.0" }, "react-dom": { singleton: true } }
})
在cart-app
中类似配置,暴露CartPage
组件。shell-app
则配置 remotes:js复制编辑
// shell-app 的 webpack.config.js
new ModuleFederationPlugin({
name: "shellApp",
remotes: {
productsApp: "productsApp@http://localhost:8101/remoteEntry.js",
cartApp: "cartApp@http://localhost:8102/remoteEntry.js"
},
shared: { react: { singleton: true }, "react-dom": { singleton: true } }
})
- 注意各应用配置的
port
不同(8100系列),以便本地同时运行。
- 主应用集成: 在
shell-app
的路由配置中,设置/products
路径加载远程组件:
// ShellApp.jsx (主应用路由示例)
import { lazy, Suspense } from 'react';
const ProductsPage = lazy(() => import('productsApp/ProductsPage'));
const CartPage = lazy(() => import('cartApp/CartPage'));
// ...
<Routes>
<Route path="/products" element={
<Suspense fallback={<div>加载产品列表...</div>}>
<ProductsPage />
</Suspense>
}/>
<Route path="/cart" element={
<Suspense fallback={<div>加载购物车...</div>}>
<CartPage />
</Suspense>
}/>
</Routes>
这样,当用户访问/products
路由时,主应用会动态加载http://localhost:8101/remoteEntry.js
并渲染其中的ProductsPage
组件。/cart
类似。
- 运行测试: 分别启动三个应用的开发服务器,验证在主应用中可以成功查看产品列表和购物车页面,并且在Network面板看到相应的远程模块JS被按需加载。确认共享的React只加载了一份(在Network中应只见到一个react相关的JS文件)。
通过这个案例,可以体会到 Module Federation 的开发流程与普通单体应用并无太大差异,主要是配置和加载方式的不同。开发期间,各团队仍然可以各自运行自己应用进行调试;在需要整合时,运行所有服务即可在主应用中看到完整功能。
案例2:在 React 项目中的应用
React 社区对模块联邦的支持相当成熟,上述案例即为典型代表。在实际工程中,还有一些经验值得分享:
- 对于使用 Create React App(CRA)构建的项目,启用 Webpack5 模块联邦需要弹出配置或使用社区的
craco
等工具定制 webpack 配置,加入 ModuleFederationPlugin。另有社区项目@module-federation/nextjs-mf
可用于 Next.js 应用,vite-plugin-federation
用于 Vite 等,在不同构建工具中实现类似模块联邦的支持。 - React 项目常常需要共享状态或路由等。可以考虑将全局状态管理库(如 Redux、zustand)作为 shared 依赖,这样Host与Remote可以操作同一个全局 store。当然这对版本和数据结构要求严格,升级需要小心。另一个做法是通过 props 或 context 在 Remote 组件间传递必要的回调,从而避免共享过多隐含状态。
- UI 框架共享:如果Remote需要使用Host的UI框架样式,比如Host用Ant Design,那么Remote也应当以shared方式使用相同版本的AntD,否则可能出现两个样式风格或冲突。实践中通常会把UI库放在Host中引入,并通过shared给Remote用,Remote自身并不单独引入样式(或仅引入CSS而不重复JS部分)。

案例3:在 Vue 项目中的应用
Vue3 同样可以使用 Webpack5 模块联邦。配置方式类似,在 ModuleFederationPlugin 的 exposes 中暴露 Vue 组件或模块,在 Host 中通过import('remoteApp/Component')
加载。需要注意的是,Vue 组件在不同应用间共享时,如果涉及到全局的Vue实例或插件,也要作为shared处理。例如多个Remote都用到了Vuex或Vue Router,应该确保它们共享使用同一个实例,而不是各自初始化各自的,这样才能在切换路由或状态时保持一致。
社区提供了示例脚手架,如vue-module-federation-plugin
,可以快速将Vue CLI项目配置为模块联邦模式。实践中,Vue微前端的常见场景是各团队维护不同的业务模块,最终通过主应用的vue-router
整合。例如企业后台系统将不同业务功能拆分为子应用,每个应用打包后部署在各自服务器,主应用运行时通过动态组件+iframe或直接模块联邦加载这些子模块的视图。相较React,Vue微前端可能更多考虑CSS隔离(如命名空间避免冲突)和全局事件总线的共享,这些都可以在模块联邦架构下通过约定加以规范。
案例4:逐步将单体前端拆分为模块化架构
假设我们有一个老的单体前端项目“电商管理后台”,其中包含订单管理、商品管理、用户管理等多个模块。现在希望逐步实现微前端改造,而不是一次性重写。模块联邦可以帮助我们做到这一点:
- 抽取模块作为独立应用: 选择一个相对独立的模块(例如“商品管理”)作为试点。新建一个基于Webpack5的前端项目,实现与老项目中该模块相同的功能和接口。这期间可以复用老项目的部分代码。确保新项目可以独立运行,通过某路径展示商品管理界面。
- 老项目作为Host加载新模块: 在老项目中,引入 ModuleFederationPlugin 配置,将新“商品管理”应用作为Remote。老项目的路由或菜单点击,改为动态导入远程模块。如果老项目本身无法升级到Webpack5(比如用的老版本构建工具),也可以采用折中办法:在老项目页面中通过插入一个
<script>
来加载远程应用的bundle,然后调用其初始化函数。这类似于手动的模块联邦。理想情况下还是升级老项目到Webpack5,使其真正成为Host。 - 同步共享依赖和UI: 确保老项目与新模块使用一致的基础库版本,例如都用Vue2或都用React17等。如果老项目样式需要继续在新模块中使用,可以把CSS通过
shared
或者公共CDN引入,让新模块看起来无缝融入老系统。 - 逐步迁移其它模块: 验证商品管理模块运行稳定后,按计划逐一将订单管理、用户管理等拆分为Remote应用。每拆出一个,都在老Host中配置并替换对应功能。当大部分模块都迁移完毕,老项目实际上就退化成一个纯粹的整合壳,主要负责路由导航和用户认证等公共部分。此时可以考虑将Host本身也升级重构成更轻量的框架。
- 最终拆除旧代码: 当所有模块均以Remote形式提供后,老项目中冗余的旧模块代码即可移除。如果老项目原本很庞大,经过这个渐进过程,最终演变成一个全新的微前端架构系统,且在整个过程中保持了业务连续性。
在这个过程中,需要仔细处理过渡期的兼容:比如部分功能还未拆走,老代码和新Remote可能需要同时存在。可以通过功能开关或路由配置来控制哪些用新的Remote,哪些暂时用老代码。当Remote模块遇到问题无法使用时,可以临时切换回老代码作为应急方案。这样逐步替换、双跑验证,风险会小很多。实际企业中,渐进迁移通常是唯一可行的方案,模块联邦为这种演进式重构提供了技术支撑。
7. 常见问题及解决方案
尽管模块联邦功能强大,但在实际使用中也会遇到一些常见的问题。下面列出几个典型问题及其解决思路。
问题1:模块加载失败 – 表现为在浏览器控制台出现错误,例如Failed to load remote module
或cannot read property 'get' of undefined
等。常见原因和解决方案:
- 远程入口路径错误: 检查 Host 配置的
remotes
URL是否正确可访问。必须确保 Remote 部署后能够通过该URL获取到remoteEntry.js
。解决方法是修正URL或部署路径。如在本地开发时端口要对应正确,在生产环境中可能需要使用完整域名路径。 - 远程 name 不匹配: Host 配置中的远程名必须和 Remote 配置中的
name
完全一致(包括大小写)。如果不匹配,会导致加载的脚本找不到正确的容器注册名称。解决方案是统一两边使用相同字符串。 - 跨域/CORS 问题: 若 Host 和 Remote 不在同一域,浏览器加载 remoteEntry.js 时需要通过CORS校验。确保远程服务器设置了允许跨域(Access-Control-Allow-Origin)。否则会被浏览器拦截。解决方案是在远程静态服务器配置CORS头。
- 网络超时或断网: 远程服务器不可达时会加载失败。应在代码中对
import()
添加错误处理,例如添加.catch
逻辑,在UI上提示用户或重试加载。
问题2:共享依赖版本冲突 – 当 Host 和 Remote 对同一库依赖版本不一致,可能出现错误或隐蔽的bug。例如 Host 使用lodash 4.X,而Remote打包时用了lodash 3.X。如果都标记为shared且未strict,运行时可能加载了lodash4供Remote使用,但Remote代码期望lodash3的某些老接口,可能导致运行时错误。解决方案:
- 统一版本: 尽量升级使双方依赖版本一致。如果一方升级成本大,评估是否可以让另一方暂时兼容旧版本接口。
- strictVersion提示冲突: 打开strictVersion可以在控制台明确报出版本不匹配,使问题显性化,然后有针对性地调整版本或关闭共享。必要时可以暂时将该依赖移出shared,让Remote自带旧版本,但需注意潜在重复问题。
- 降级兼容: 如果Remote必须用旧版本,可以考虑在Host提供一个polyfill或适配,使其提供旧版本所需的函数。或者Remote在检测到新版本库时,动态调整一些调用(这需要开发者在Remote代码中做兼容判断)。
- 避免全局污染: 当版本冲突不可避免地需要同时存在时,确保它们不会互相污染。例如两个版本的Moment库,则不要使用全局Moment对象,而是在模块内部引用自己的,以免搞混。不过这种情况尽量避免,能统一还是统一。
问题3:性能问题 – 可能体现在首屏加载变慢,或切换某模块时出现明显延迟。排查思路和优化:
- 首屏加载资源过多: 检查是否无意中在主应用中同步引入了大量远程模块(例如没有使用懒加载而直接import了)。应将远程import改为动态,首屏只加载必要部分。另外Host自身的bundle也应按路由分拆,避免主包过大。
- 远程模块体积大: 远程应用虽然独立,但如果其bundle很大,加载时仍然耗时。优化remote的webpack配置,开启压缩、去除source map、合理分拆内部chunks。如果多个Remote都用了相同的大库而没有共享成功,会导致重复下载,需通过shared配置解决。
- 网络延迟: 如果 Remote 托管在较远的服务器,加载其资源可能慢。可考虑将静态资源放到CDN,用户访问时就近获取。另外利用HTTP/2多路复用、开启压缩(gzip/br)等常规优化也适用。对于极端关键的模块,可以牺牲一点独立性,把它直接打包进Host以保证速度(权衡取舍下)。
- 内存和销毁: 频繁加载卸载远程模块可能造成内存占用升高,要留意远程模块的清理。如果Remote里有全局单例(如创建了window上的对象),在模块卸载时可能还留在内存。确保Remote退出界面时移除事件监听、定时器等。虽然浏览器不会真正卸载已加载的JS文件,但良好的资源管理有助于长期运行稳定。
问题4:调试困难 – 微前端架构引入后,问题可能出在Host也可能在Remote,调试需要一点技巧:
- 可以在浏览器DevTools的Source里找到远程加载的脚本(一般会显示为webpack://remoteApp/...)。利用断点调试远程模块的代码逻辑。
- 使用Webpack的
publicPath
和sourceMapFilename
配置,确保远程模块的source map正确指向可访问的位置,这样在本地开发能调试源码而不是打包后的代码。 - 如果怀疑共享依赖问题,可在运行时打印
__webpack_share_scopes__
对象,检查里面的内容版本。这是Webpack内部存储共享模块的地方,能看出哪个库用了哪个版本。
问题5:部署更新问题 – 例如 Remote 更新后,Host 加载的还是旧缓存。解决方案如前文所述:给文件名加哈希或版本号,或让Host加载路径包含版本,每次发布Remote时更新Host配置指向新路径。也可以采取自动发现更新的机制,比如Host定期拉取远程的版本信息,如果有新版本提示用户刷新,或直接刷新iframe等(取决于架构实现)。
最后,模块联邦作为一项新技术,社区生态仍在发展中。遇到问题时,参考官方文档、社区案例非常重要。例如Webpack官方的Module Federation文档、以及各种框架下的实践文章都提供了宝贵的经验。在解决具体问题的同时,也要根据项目特点权衡模块联邦是否最佳方案——对于团队协作明确、发布频率低的项目,简单的组件库共享或monorepo也许足够;但当你需要独立演进的微前端时,Webpack 5 模块联邦将是强大的利器,使开发者能够“各自为战,最终合体”,实现理想的软件架构。