- 示例工程
【免费下载链接】module-federation-examples
Implementation examples of module federation , by the creators of module federation
本指南以 module-federation-examples 仓库中的 nested-remote 示例为核心,讲解如何用 Module Federation 实现「远程组件内部再加载远程组件」的嵌套场景:Host(app1)异步加载 app2 暴露的ButtonContainer,而 app2 自身又异步加载 app3 暴露的Button。读完本文,你将掌握三应用链式联邦的架构设计、webpack 配置要点、React.lazy + Suspense 的异步加载写法,以及如何用 Playwright 端到端验证嵌套链路是否真的打通。
什么是嵌套远程(Nested Remote)
常规的 Module Federation 演示通常只覆盖「一个 Host 加载一个 Remote」的两层结构。但真实业务中,被加载的远程应用本身也可能是一个独立的应用,它内部还需要继续依赖别的远程应用——这就形成了嵌套(Nested)远程链路。
nested-remote 示例用三个应用精确演示了这一场景:
app1:Host 应用,异步加载来自app2的ButtonContainer组件;app2:独立可运行的应用,对外暴露ButtonContainer组件,而该组件内部又异步加载来自app3的Button;app3:独立可运行的应用,对外暴露最底层的Button组件。
链路可以简化为app1 →(remote) app2 →(remote) app3。关键在于:app2 既是被 app1 消费的 Remote,同时也是消费 app3 的 Host(或者说中间层 Remote)。这正是嵌套联邦与普通联邦的本质区别——中间层应用必须同时具备exposes和remotes两种能力。
一键启动三个应用
在仓库根目录或nested-remote目录下执行:
pnpm run start根级 package.json 中start脚本定义为:
"start": "pnpm --filter nested_app* --parallel start"它会通过 pnpm workspace 并行启动nested_app1、nested_app2、nested_app3三个子包(分别对应 app1、app2、app3 的 package.json、package.json、package.json,它们均被 pnpm-workspace.yaml 的nested-remote/*规则纳入 workspace)。每个子包的start脚本实际执行webpack-cli serve,即通过 webpack-dev-server 启动开发服务,三个应用分别监听:
| 端口 | 应用 | 角色 |
|---|---|---|
| http://localhost:3001 | app1 | HOST(宿主) |
| http://localhost:3002 | app2 | STANDALONE REMOTE(独立远程,兼中间层) |
| http://localhost:3003 | app3 | STANDALONE REMOTE(独立远程,叶子节点) |
浏览器打开localhost:3001,页面会依次显示 app1 的内容、app2 的容器以及来自 app3 的按钮,证明嵌套链路在运行时真实打通。此外,每个子包还提供build(webpack --mode production)与serve(serve dist -p 300x)脚本,可用于生产构建后静态托管。
架构拆解:三个应用的 webpack 联邦配置
三个应用的 webpack 配置均使用@module-federation/enhanced/webpack提供的ModuleFederationPlugin,但职责各不相同。
app1:纯 Host,声明对 app2 的远程依赖
app1/webpack.config.js 的核心联邦配置:
plugins: [ new ModuleFederationPlugin({ experiments: { asyncStartup: true }, name: 'app1', shareStrategy: 'loaded-first', remotes: { app2: `app2@${getRemoteEntryUrl(3002)}`, }, shared: { react: { singleton: true }, 'react-dom': { singleton: true } }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ],要点说明:
remotes.app2指向app2@//localhost:3002/remoteEntry.js(由getRemoteEntryUrl(3002)生成),即 app2 对外发布的联邦入口文件;shareStrategy: 'loaded-first'表示共享依赖采用「先加载者优先」的解析策略,结合shared中react、react-dom的singleton: true,保证整个链路中 React 只存在一份实例,避免 hooks 状态错乱;experiments: { asyncStartup: true }启用异步启动能力,让远程模块加载与本地入口初始化可以并行,是嵌套场景下提升首屏体验的关键实验特性。
app2:既是 Remote 又是 Host 的中间层
app2/webpack.config.js 同时配置了exposes与remotes,这是嵌套联邦的核心:
plugins: [ new ModuleFederationPlugin({ experiments: { asyncStartup: true }, name: 'app2', shareStrategy: 'loaded-first', filename: 'remoteEntry.js', exposes: { './ButtonContainer': './src/ButtonContainer', }, remotes: { app3: `app3@${getRemoteEntryUrl(3003)}`, }, shared: { react: { singleton: true }, 'react-dom': { singleton: true } }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ],filename: 'remoteEntry.js'指定远程入口文件名,Host(app1)的remotes.app2正是解析这个地址;exposes['./ButtonContainer']将本地./src/ButtonContainer组件暴露给 app1;remotes.app3声明 app2 自身对 app3 的远程依赖——这意味着当 app1 加载app2/ButtonContainer时,Module Federation 运行时还会按需再去拉取 app3 的remoteEntry.js。
app3:叶子 Remote,只暴露组件
app3/webpack.config.js 是链路的末端,不需要声明任何remotes:
plugins: [ new ModuleFederationPlugin({ experiments: { asyncStartup: true }, name: 'app3', shareStrategy: 'loaded-first', filename: 'remoteEntry.js', exposes: { './Button': './src/Button', }, shared: { react: { singleton: true }, 'react-dom': { singleton: true } }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ],三个配置还共享两个通用细节:output.publicPath: 'auto'让资源 URL 根据当前页面地址自动推导,保证开发(webpack-dev-server)与部署(静态托管)环境都能正确加载;devServer.headers统一开启了Access-Control-Allow-Origin: *等 CORS 头,允许跨端口加载远程脚本。
getRemoteEntryUrl:本地与 CodeSandbox 环境的自适应
三个配置文件都定义了同名的getRemoteEntryUrl(port)工具函数(见 app1/webpack.config.js):
function getRemoteEntryUrl(port) { const { CODESANDBOX_SSE, HOSTNAME = '' } = process.env; // Check if the example is running on codesandbox if (!CODESANDBOX_SSE) { return `//localhost:${port}/remoteEntry.js`; } const parts = HOSTNAME.split('-'); const codesandboxId = parts[parts.length - 1]; return `//${codesandboxId}-${port}.sse.codesandbox.io/remoteEntry.js`; }它优先返回//localhost:<port>/remoteEntry.js;当检测到CODESANDBOX_SSE环境变量(运行在 CodeSandbox 中)时,则从HOSTNAME中解析出沙箱 ID,拼出对应的sse.codesandbox.io地址。该函数体现了示例对「本地运行 + 在线沙箱」双环境的兼容设计。
组件级异步加载:React.lazy + Suspense 的逐层透传
嵌套远程的加载不是一次性完成的,而是由每一层的组件各自负责异步拉取下一层。
app1 的入口组件
app1/src/App.js:
import React from 'react'; const RemoteButtonContainer = React.lazy(() => import('app2/ButtonContainer')); const App = () => ( <div> <h1>Nested</h1> <h2>App 1</h2> <p>app 1 body</p> <React.Suspense fallback="Loading Button Container"> <RemoteButtonContainer /> </React.Suspense> </div> );import('app2/ButtonContainer')中的app2前缀对应 webpack 配置里remotes的 key,webpack 会将其编译为对 app2remoteEntry.js的运行时加载调用。React.lazy将远程模块包装成懒加载组件,外层用React.Suspense提供「Loading Button Container」占位文案。
app2 的 ButtonContainer:中间层组件的二次远程加载
app2/src/ButtonContainer.js 是嵌套的核心组件:
import React from 'react'; const RemoteButton = React.lazy(() => import('app3/Button')); const style = { padding: 12, backgroundColor: '#cccccc', }; const ButtonContainer = () => ( <div style={style}> App 2 Container <br /> <br /> <React.Suspense fallback="Loading Button"> <RemoteButton /> </React.Suspense> </div> );它同时扮演两个角色:作为 app2 暴露给 app1 的「导出组件」,以及作为消费 app3 的「宿主组件」。组件内部再次使用React.lazy(() => import('app3/Button'))触发对 app3 的加载,并套上独立的Suspense边界。于是运行时的加载时序是:app1 先拉取 app2 的 remoteEntry → 渲染 ButtonContainer → 触发 app3 的 remoteEntry 加载 → 渲染 Button。
app3 的 Button:链路末端的真实渲染
app3/src/Button.js 最终渲染带 aquamarine 背景色的按钮:
import React from 'react'; const style = { padding: 12, backgroundColor: 'aquamarine', }; const Button = () => <button style={style}>App 3 Button</button>; export default Button;每个应用都可独立运行
注意 app2 与 app3 的入口文件 app2/src/App.js 与 app3/src/App.js 分别直接引用了本地组件(import ButtonContainer from './ButtonContainer'、import LocalButton from './Button'),并同样通过 index.js 中的ReactDOM.render挂载到各自的#root。这说明中间层与叶子节点既是可被远程消费的联邦组件提供者,又是可独立访问localhost:3002、localhost:3003单独运行验证的完整应用——这正是示例强调的 "STANDALONE REMOTE" 定位。
端到端验证:Playwright 如何证明嵌套链路生效
仅凭浏览器肉眼观察不足以自动化回归,仓库为每个应用都编写了 Playwright 用例(e2e 目录),从「元素存在性」与「样式来源」两个维度证明嵌套加载真实生效。
checkApp1.spec.ts 访问 3001 端口并断言:
- 页面上存在可见的按钮,且文本为
App 3 Button——只有 app1 → app2 → app3 整条链路全部加载成功,这个来自 app3 的按钮才会出现在 app1 的页面里; - 根容器包含
App 2 Container与app 1 body文本; - 样式断言中,
App 2 Container容器的背景色为rgb(204, 204, 204)(对应源码中的#cccccc),按钮背景为rgb(127, 255, 212)(对应aquamarine),这些内联样式分别来自 app2 与 app3 的组件源码,可作为「组件确实来自对应远程应用」的旁证。
checkApp2.spec.ts 与 checkApp3.spec.ts 则分别验证 3002、3003 两个独立应用能自行渲染按钮及对应样式,确保远程应用脱离 Host 也能独立工作。
playwright.config.ts 通过webServer配置在跑测试前自动执行pnpm run start并等待http://localhost:3001就绪,测试命令为:
pnpm run test:e2e # 或 pnpm run e2e:ci(分别对应根级 package.json 中的test:e2e与e2e:ci脚本,后者以--reporter=list输出。)关于该仓库 Playwright 测试基础设施的通用约定(公共辅助类BaseMethods、选择器、常量定义等),可参考 playwright-e2e/README.md。
关键配置速查与实战建议
| 配置项 | 取值(本示例) | 作用 |
|---|---|---|
name | app1/app2/app3 | 联邦应用全局唯一标识,也是 remote 引用的命名空间 |
filename | remoteEntry.js | 远程入口文件名,被上游remotes引用 |
exposes | ./ButtonContainer、./Button | 声明对外暴露的模块及本地源文件映射 |
remotes | app2@...、app3@... | 声明本应用依赖的远程入口地址 |
shared | react、react-dom均singleton: true | 跨应用共享单例依赖,避免多实例冲突 |
shareStrategy | loaded-first | 共享依赖解析策略:已加载版本优先 |
experiments.asyncStartup | true | 异步启动,允许远程加载与本地初始化并行 |
output.publicPath | auto | 自动推导资源公共路径,适配多环境 |
基于该示例的实践要点:
- 中间层必须同时具备 exposes 与 remotes:app2 是嵌套链路的枢纽,只暴露不消费(或反之)都会使链路断裂;
- 共享依赖统一配置 singleton:三个应用对
react、react-dom采用一致的共享策略,是从源码结构看该示例保证嵌套场景下 React 单例运行的关键设计; - 为每个远程组件提供 Suspense 边界:逐层
React.lazy+Suspense让每一跳的加载状态都有明确的占位反馈,避免整页白屏; - 保持 Remote 可独立运行:app2、app3 各自的入口均直接渲染本地组件,让远程应用既能被消费、又能单独调试;
- 用端到端测试固化链路契约:断言「出现在 Host 页面中的按钮文本与样式来自末端应用」,即可自动守护嵌套链路不被破坏。
- 示例工程
【免费下载链接】module-federation-examples
Implementation examples of module federation , by the creators of module federation
相关推荐
module-federation-examples basic-host-remote:用 Modern.js 搭建最小 Host-Remote 模块联邦示例
module federation examples basic host remote:用 Modern.js 搭建最小 Host Remote 模块联邦示例
示例工程Module Federation 实战:用 Vite + Angular 构建 host/remote 微前端(module-federation-vite-angular 示例解析)
Module Federation 实战:用 Vite + Angular 构建 host/remote 微前端(module federation vite
示例工程基于 Rsbuild 的 Create React App 微前端示例:Host 与 Remote 的 Module Federation 实践指南
基于 Rsbuild 的 Create React App 微前端示例:Host 与 Remote 的 Module Federation 实践指南 导读 本文
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考