- 示例工程
【免费下载链接】module-federation-examples
Implementation examples of module federation , by the creators of module federation
comprehensive-demo-react18 是 module-federation-examples 仓库中的一个多框架综合示例,其中位于 app-01/src/docs/Tabs.md 的文档,完整演示了「宿主应用(App #1)通过 Module Federation 远程加载 App #2 的 Tabs 组件,并让浏览器路由与 Tab 状态实时同步」的实战方案。本文将以该文档为主线,结合仓库内真实源码与 E2E 测试,逐层拆解远程 Tab 组件如何实现、如何与路由联动、如何按需懒加载第三个应用的样式化组件,帮助你掌握一套可复用的「远程组件 + 路由同步 + 懒加载」联邦化开发模式。
一、文档背后的演示场景:App #1 中的 Routing Demo
Tabs.md 位于宿主应用 App #1 的文档目录中,它描述了以下四个核心事实:
- 页面中的 Tab 组件是从
bravo-app(即 App #2,运行在 3002 端口)远程导入的; - 浏览器的路由会随激活的 Tab 在
/routing/foo与/routing/bar之间切换; - 直接打开 http://localhost:3002(App #2 的根页面)可以看到完全相同的 Tab 组件;
- "Bar" 这个 Tab 内还按需懒加载了来自 UI Library 演示(App #3,3003 端口)的 styled-components
Button,且仅在 Bar 被渲染时才加载。
这正是 Module Federation 中「同一远程组件既可作为宿主页面内嵌模块、又可作为远程应用自身页面模块」的双重用法,也展示了跨应用按需加载的完整链路:App #1 → App #2(Tabs)→ App #3(Button)。
二、文档如何在页面中呈现:routing-page.jsx 与 Markdown 渲染链路
Tabs.md 之所以能成为路由演示页的说明文档,是因为 App #1 在构建时通过raw-loader将.md文件作为字符串导入(见 app-01/webpack.config.js),再由 app-01/src/Markdown.jsx 使用markdown-to-jsx渲染成 Material UI 风格的排版。
路由演示页 routing-page.jsx 把「文档」与「远程组件」放在同一页面:
import Markdown from 'markdown-to-jsx'; import Page from '../Page'; import React from 'react'; import Tabs from '../docs/Tabs.md'; const RoutedTabs = React.lazy(() => import('app_02/Tabs')); const RoutingPage = () => ( <Page title="Routing Demo"> <Markdown>{Tabs}</Markdown> <React.Suspense fallback="Loading Tabs..."> <RoutedTabs /> </React.Suspense> </Page> );这里import('app_02/Tabs')是app_02这个远程容器(remote container)的异步导入调用,配合React.lazy与React.Suspense,实现了远程组件的懒加载:只有当用户进入/routing路由时,Tabs 组件才会被拉取并渲染。该页面被挂载在 app-01/src/Routes.jsx 的/routing路径下。
三、远程 Tab 组件的实现:路由驱动切换的 Tabs.jsx
文档所说的「路由随 Tab 变化」并非空话,而是由 App #2 中的 Tabs.jsx 通过react-router-dom的 Hook 实现的。它用useRouteMatch()取得当前页面的基础路径(在 App #1 内即/routing),用useHistory()在切换 Tab 时改写 URL,用useLocation()读取当前路径作为 Tab 的激活值:
const match = useRouteMatch(); const history = useHistory(); const location = useLocation(); const { path: rootPath } = match; const handleChange = (event, newValue) => { history.push(newValue); }; <Tabs value={location.pathname} onChange={handleChange}> <Tab label="Foo" value={`${rootPath}/foo`} /> <Tab label="Bar" value={`${rootPath}/bar`} /> </Tabs>随后通过Switch/Route将路由与内容绑定:/routing精确匹配时重定向到/routing/foo,/routing/foo渲染 "Foo Content",/routing/bar渲染 "Bar Content"。
关键点在于:这套组件并不关心自己运行在哪个应用里。它从match.path推导基础路径,因此在 App #1 的/routing下表现为/routing/foo、/routing/bar;而当它作为 App #2 根页面的一部分渲染时(见 app-02/src/App.jsx),基础路径退化为空,路由就变成/foo、/bar。文档中「打开 3002 端口能看到同样的 Tab 组件」正是由此而来。
四、Bar Tab 的按需懒加载:跨应用加载 styled-components Button
文档还强调了一个容易被忽略的细节:Bar 内容里的Button来自 UI Library 演示(App #3),并且「仅在 Bar 被渲染时懒加载」。这在 Tabs.jsx 的顶部和 Bar 路由内均有体现:
const Button = React.lazy(() => import('app_03/Button'));Bar 路由的内容中嵌套了React.Suspense:
<Route path={`${rootPath}/bar`}> <Typography component="div"> <Box p={3}> Bar Content <React.Suspense fallback={null}> <Button>Bar Button</Button> </React.Suspense> </Box> </Typography> </Route>只有路由落在/routing/bar(或 App #2 根部的/bar)时,app_03/Button才会被真正加载。该按钮是 app-03/src/Button.jsx 中一个典型的 styled-components 样式化组件:
const Button = styled.button` display: inline-block; border-radius: 3px; padding: 0.5rem 0; margin: 0.5rem 1rem; width: 11rem; background: palevioletred; color: white; font-size: 1rem; font-family: sans-serif; `;于是形成了「宿主 → 远程 Tab → 更远程的 Button」两级懒加载链条,验证了 Module Federation 支持嵌套联邦(remote 再消费 remote)的能力。
五、支撑这一切的联邦配置:exposes、remotes 与 shared
5.1 App #1:宿主兼聚合方
app-01/webpack.config.js 中的ModuleFederationPlugin声明了四个远程依赖,其中就包含 App #2 与 App #3:
remotes: { app_02: 'app_02@http://localhost:3002/remoteEntry.js', app_03: 'app_03@http://localhost:3003/remoteEntry.js', app_04: 'app_04@http://localhost:3004/remoteEntry.js', app_05: 'app_05@http://localhost:3005/remoteEntry.js', },同时它以exposes对外提供./SideNav与./Page,供 App #2 反向消费(见下文 App #2 的 App.jsx)。
5.2 App #2:既是被消费方,又是消费方
app-02/webpack.config.js 中,name: 'app_02'与filename: 'remoteEntry.js'定义了远程容器的身份与入口文件;exposes声明了对外暴露的模块:
exposes: { './Dialog': './src/Dialog', './Tabs': './src/Tabs', },remotes则声明了它自身的依赖——App #1 的Page组件与 App #3 的Button:
remotes: { app_01: 'app_01@http://localhost:3001/remoteEntry.js', app_03: 'app_03@http://localhost:3003/remoteEntry.js', },App #2 的根应用 App.jsx 正是通过import('app_01/Page')复用 App #1 的Page外壳(顶栏 + 内容区),再把本地Dialog与Tabs渲染进去,从而与 App #1 的页面观感完全一致。
5.3 共享依赖:singleton 与 eager
为避免 React 双实例问题,两端配置都对核心依赖做了单例化处理。以 App #2 为例:
shared: { ...deps, '@material-ui/core': { singleton: true }, 'react-router-dom': { singleton: true }, 'react-dom': { singleton: true, requiredVersion: false, eager: true }, react: { singleton: true, requiredVersion: false, eager: true }, },singleton: true:保证联邦各应用共用同一份react、react-dom、@material-ui/core与react-router-dom,避免 hooks 状态错乱;requiredVersion: false:放宽版本校验,允许跨应用版本不一致时仍可共享;eager: true:让核心依赖在容器启动时同步就绪,避免加载时序问题;- 此外
experiments: { asyncStartup: true }开启了异步启动实验特性,shareStrategy: 'loaded-first'指定共享依赖采用「先加载者优先」策略。
值得说明的是,App #2 同时提供了 webpack 与 rspack 两套构建配置(webpack.config.js 与 rspack.config.js),二者在联邦配置上保持了一致,仅构建器与 loader 不同,便于对比两种工具链下的联邦实现。此外,两端 devServer 都开启了 CORS 头(Access-Control-Allow-Origin: *),这是浏览器端跨端口拉取remoteEntry.js与联邦模块的必要前提。
六、文档中「路由即 URL」的可验证性:Playwright E2E 测试
文档所描述的行为已被仓库内的 E2E 测试固化。在 comprehensive-demo-react18/e2e/checkApp1.spec.ts 中,routing page renders tabs用例完整验证了整条链路:
test('routing page renders tabs', async ({ page }) => { await page.goto(`${base}/#/routing/foo`); ... await expect(page.getByRole('tab', { name: 'Foo' })).toBeVisible(); await expect(page.getByText('Foo Content')).toBeVisible(); await page.getByRole('tab', { name: 'Bar' }).click(); await expect(page.getByText('Bar Content')).toBeVisible(); const barButton = page.getByRole('button', { name: 'Bar Button' }); await expect(barButton).toBeVisible(); await expect(barButton).toHaveCSS('background-color', 'rgb(219, 112, 147)'); });该用例同时断言了 Tabs.md 中的全部四段文字(routingParagraphs,见 checkApp1.spec.ts),并验证了:
- 路由进入
#/routing/foo时 Foo Tab 激活、Foo 内容可见; - 点击 Bar Tab 后 Bar 内容可见,且 Bar Button 懒加载成功;
rgb(219, 112, 147)正是 App #3 中palevioletred的解析结果,证明远程样式化按钮确实渲染生效。
同一测试文件还以{ name: 'Routing', hash: '#/routing/foo' }断言了侧边栏中 Routing 入口的链接(checkApp1.spec.ts),并将#/routing/foo作为文档锚点链接的一部分(checkApp1.spec.ts),与文档「浏览器路由是 /routing/<foo|bar>」的描述相互印证。
七、如何运行与体验
在 comprehensive-demo-react18 目录下按序执行:
yarn install pnpm run start随后打开 http://localhost:3001,从左侧导航进入Routing页面即可复现本文所述场景;再打开 http://localhost:3002 对比同一 Tabs 组件在远程应用根部的表现。整个 demo 由五个应用构成:App #1(3001,宿主外壳)→ App #2(3002,Material UI 组件提供方)→ App #3(3003,styled-components UI 库)→ App #4(3004,Svelte)→ App #5(3005,LitElement)。运行 E2E 测试可执行pnpm test:e2e,或使用pnpm test:e2e:ui打开交互式调试界面(见 comprehensive-demo-react18/package.json)。
八、小结:从本文可复用的三件事
通过 Tabs.md 与配套源码,可以提炼出三种可直接迁移到自有项目的实践:
- 远程组件的路由自适应:远程组件用
useRouteMatch().path而非硬编码路径推导基础路由,就能在同一份代码下同时服务于宿主子路由(/routing/foo)与远程应用根路由(/foo); - 两级懒加载:
React.lazy+Suspense可以让「宿主 → 远程 → 再远程」的嵌套依赖按需加载,配合exposes/remotes的容器声明即可实现组件级代码拆分; - 以文档驱动演示:将 Markdown 说明文件作为页面的一部分(
raw-loader导入 +markdown-to-jsx渲染),再配合同页面的可交互远程组件与 E2E 断言,让「文档描述、代码实现、自动化验证」三者保持一致。
如需继续深入,可对照阅读同目录下的 Dialog.md(React Portal 场景的远程 Dialog)与 UiLibrary.md(独立 CSS 方案下的远程 Button),它们共同构成了 comprehensive-demo-react18 中「跨应用组件复用」的完整示例集。
- 示例工程
【免费下载链接】module-federation-examples
Implementation examples of module federation , by the creators of module federation
相关推荐
跨应用远程 Tab 组件实战:基于 module-federation-examples 的嵌套路由与惰性加载解析
跨应用远程 Tab 组件实战:基于 module federation examples 的嵌套路由与惰性加载解析 导读 本文以 module federati
示例工程Module Federation 实战:在 comprehensive-demo-react16 中懒加载远程 Dialog 组件并用 React Portal 渲染
Module Federation 实战:在 comprehensive demo react16 中懒加载远程 Dialog 组件并用 React Porta
示例工程module-federation-examples 之 comprehensive-demo-react18:React 18 多框架模块联邦综合演示实战指南
module federation examples 之 comprehensive demo react18:React 18 多框架模块联邦综合演示实战指南
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考