ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

使用 Vite 构建 React 模块联邦应用:module-federation-examples 中 host 与 remote 实战指南

使用 Vite 构建 React 模块联邦应用:module-federation-examples 中 host 与 remote 实战指南
  • 示例工程

【免费下载链接】module-federation-examples

Implementation examples of module federation , by the creators of module federation

项目地址:https://gitcode.com/gh_mirrors/mo/module-federation-examples
点击查看免费下载

module-federation-vite-react是 module-federation-examples 仓库中演示Vite 与 React 组合下的模块联邦(Module Federation)的最小可运行示例:一个 Vite 构建的 host 应用在 4173 端口运行,通过运行时远程加载另一个 Vite 构建的 remote 应用(4174 端口),二者共享同一份 React 实例。读完本文,你将掌握在 Vite 项目中配置@module-federation/vite插件的完整参数、理解 host 端remotes与 remote 端exposes的对应关系,并能在本地一键启动并验证跨应用组件加载。

项目概览:Vite 场景下的 host/remote 双应用架构

官方 README 对该示例的定位非常简洁——"React host and remote"(README.md)。这个目录展示的是模块联邦经典的双应用拓扑在 Vite 生态中的落地方式:

  • host(宿主):运行在http://localhost:4173/,负责页面外壳,并在运行时按需拉取远端组件;
  • remote(远端):运行在http://localhost:4174/,通过exposes对外暴露自己的 React 组件;
  • shared(共享目录):位于module-federation-vite-react/shared/,是宿主与远端共用的代码与配置层面共享空间。

整套结构由根目录的 package.json 统一编排,通过 pnpm workspace 过滤(pnpm --filter module-federation-vite-react-*)同时管理 host 与 remote 两个子包。之所以强调 "Vite + React is now possible"(项目描述语),是因为模块联邦最初是 webpack 的能力,本示例证明了借助@module-federation/vite插件,Vite 构建的 React 应用同样可以成为模块联邦中的 host 与 remote。

快速开始:两条命令跑通整个示例

按照 README.md 的 Getting started 一节,在module-federation-vite-react目录下依次执行:

npm run install:deps npm run preview

然后打开浏览器访问http://localhost:4173/,即可看到 host 应用与远端加载的 remote 应用同时渲染在同一页面。

两条命令背后的实际动作值得拆解(见 package.json):

命令实际执行内容作用
install:depspnpm --filter module-federation-vite-react-* install只为 host、remote 两个子包安装依赖
postinstall:depspnpm --filter module-federation-vite-react-* run build依赖安装完成后自动触发一次构建,保证后续 preview 可用
previewpnpm --filter module-federation-vite-react-* preview分别构建并启动 host(4173)与 remote(4174)的静态预览服务
devpnpm --filter module-federation-vite-react-* dev以开发模式同时启动两个应用的 Vite dev server
predevnode ../scripts/track-demo-run.js开发模式启动前的仓库级演示埋点脚本(scripts/track-demo-run.js)

其中 host 与 remote 各自的preview脚本(见 host/package.json 与 remote/package.json)均为npm run build && vite preview --port 417x,即先执行tsc && vite build完成 TypeScript 检查与产物构建,再用vite preview提供静态服务。因此npm run preview一次即可让两个端口同时就绪。

目录结构:三个子目录的分工

module-federation-vite-react/ ├── docs/screenshot.png # README 中的运行效果截图 ├── host/ # 宿主应用(端口 4173) │ ├── src/ │ │ ├── components/Counter.tsx │ │ ├── App.tsx # 通过 lazy 动态加载远端组件 │ │ ├── main.tsx # ReactDOM.createRoot 入口 │ │ └── vite-env.d.ts │ ├── index.html │ ├── package.json │ └── vite.config.ts # host 端联邦配置 ├── remote/ # 远端应用(端口 4174) │ ├── src/ │ │ ├── components/Counter.tsx │ │ ├── App.tsx # 被 exposes 暴露的组件 │ │ ├── environment.ts # 构建期由插件动态生成的产物 │ │ └── main.tsx │ ├── index.html │ ├── package.json │ └── vite.config.ts # remote 端联邦配置 └── shared/ └── shared.ts # 两应用共享的类型/状态模块

两侧源码几乎对称,差异集中在联邦配置与各自承载的职责上。

host 端配置详解:remotes声明远程来源

host 的联邦配置位于 host/vite.config.ts:

import { federation } from '@module-federation/vite'; import react from '@vitejs/plugin-react'; import { defineConfig } from 'vite'; import { dependencies } from './package.json'; export default defineConfig(() => ({ server: { fs: { allow: ['.', '../shared'] } }, build: { target: 'chrome89', }, plugins: [ federation({ name: 'host', remotes: { remote: { type: 'module', name: 'remote', entry: 'http://localhost:4174/remoteEntry.js', entryGlobalName: 'remote', shareScope: 'default', }, }, exposes: {}, filename: 'remoteEntry.js', shared: { react: { requiredVersion: dependencies.react, singleton: true, }, }, }), react(), ], }));

关键配置项逐一说明:

  • name: 'host':当前应用在联邦体系中的唯一标识,同时会作为构建产物中运行时模块的命名空间前缀;
  • remotes.remote:声明一个名为remote的远端。对象内部各字段:
    • type: 'module':远端产物以原生 ES Module 形式加载(Vite 场景的默认形态,区别于 webpack 常用的var/UMD 形式);
    • name: 'remote':远端应用自身声明的联邦名,必须与 remote 端配置中的name一致;
    • entry: 'http://localhost:4174/remoteEntry.js':远端入口文件的完整 URL,host 运行时通过加载该入口获取远端模块清单;
    • entryGlobalName: 'remote':远端入口在全局作用域暴露的变量名(与name对应,供运行时定位模块容器);
    • shareScope: 'default':参与共享的依赖作用域名称,两侧需保持一致;
  • exposes: {}:host 端不对外暴露任何模块,因此为空对象;
  • filename: 'remoteEntry.js':本应用生成联邦入口文件的文件名(host 自身也会生成,便于其他应用反过来引用它);
  • shared:声明共享依赖,此处将react设为singleton: true,并指定requiredVersion: dependencies.react(即从 host/package.json 读取的^18.3.1)。这样 host 与 remote 加载的是同一份 React 运行时,避免出现双 React 实例导致的 hooks 状态错乱问题。

除联邦配置外,server.fs.allow: ['.', '../shared']允许 Vite dev server 访问应用根目录之外的shared目录;build.target: 'chrome89'则把产物目标锁定到 Chrome 89 及以上的现代浏览器,保证 ES Module 等特性可用。

remote 端配置详解:exposes暴露组件

remote 的联邦配置位于 remote/vite.config.ts:

import { federation } from '@module-federation/vite'; import react from '@vitejs/plugin-react'; import { writeFileSync } from 'fs'; import { defineConfig, loadEnv } from 'vite'; import { dependencies } from './package.json'; export default defineConfig(({ mode }) => { const selfEnv = loadEnv(mode, process.cwd()); return { server: { fs: { allow: ['.', '../shared'] } }, build: { target: 'chrome89' }, plugins: [ { name: 'generate-environment', options: function () { console.info('selfEnv', selfEnv); writeFileSync( './src/environment.ts', `export default ${JSON.stringify(selfEnv, null, 2)};`, ); }, }, federation({ filename: 'remoteEntry.js', name: 'remote', exposes: { './remote-app': './src/App.tsx', }, remotes: {}, shared: { react: { requiredVersion: dependencies.react, singleton: true, }, }, }), react(), ], }; });

与 host 端形成对照的核心差异:

  • exposes: { './remote-app': './src/App.tsx' }:将src/App.tsx以remote/remote-app的模块标识对外发布。host 端import('remote/remote-app')中的remote对应remotes中声明的远端名,remote-app对应这里exposes的键;
  • remotes: {}:remote 自身不再依赖其他远端,拓扑到此为止;
  • name: 'remote'与filename: 'remoteEntry.js':与 host 端remotes声明中的name、entry文件名精确匹配,这正是两侧配置能够握手的前提。

此外 remote 端额外内嵌了一个名为generate-environment的自定义 Vite 插件:它在options钩子中通过loadEnv(mode, process.cwd())读取当前模式(development/production)下的环境变量,并把结果序列化写入src/environment.ts(仓库中的 environment.ts 即为该构建过程的生成产物)。这意味着 remote 可以按构建模式注入运行时环境信息,体现了本示例在演示联邦之外附带的环境变量处理手法。

运行时消费:lazy + Suspense 动态加载远端组件

host 在 host/src/App.tsx 中消费远端组件,使用的正是 React 标准的代码分割 API:

import { lazy, Suspense, useEffect } from 'react'; import { of, tap } from 'rxjs'; import './App.css'; import Counter from './components/Counter'; const Remote = lazy( // @ts-ignore async () => import('remote/remote-app'), ); export default () => { useEffect(() => { of('emit') .pipe(tap(() => console.log("I'm RxJs from host"))) .subscribe(); }, []); return ( <> <div className="host">...</div> <Suspense fallback="loading..."> <Remote /> </Suspense> </> ); };

要点:

  1. lazy(() => import('remote/remote-app')):remote是 host 配置里声明的远端别名,remote-app是 remote 暴露的模块键。模块联邦运行时会在首次渲染时去加载http://localhost:4174/remoteEntry.js,再解析出对应组件;
  2. Suspense fallback="loading...":远端模块未加载完成前显示占位文本,加载完成后渲染 remote 组件;
  3. 行内的@ts-ignore是因为 Vite 场景下尚未为该虚拟模块生成类型声明;
  4. host 自身还引入了rxjs(host/package.json 中rxjs: ^7.8.1),并在useEffect中执行了一次订阅打印,用于演示 host 独立依赖正常工作的同时不干扰联邦加载。

remote 端 remote/src/App.tsx 是一个自包含的组件:渲染 "I'm the remote app" 标题和远程版 Counter,并在挂载时打印Remote useEffect。host 与 remote 各自拥有独立的Counter组件实现(host/src/components/Counter.tsx 与 remote/src/components/Counter.tsx),点击后分别累加各自的count状态,互不干扰——这也是验证两套 React 组件在同一页面共存的最直观方式。

共享目录与 TypeScript 路径别名

示例在应用目录之外维护了一个 shared/shared.ts:

export const state = { message: '<no message>', };

它被两侧的 TypeScript 配置通过paths统一映射为shared别名,例如 host/tsconfig.json 与 remote/tsconfig.json 中均有:

"baseUrl": ".", "paths": { "shared": ["../shared/shared.ts"] }

为了让 Vite 在 dev 模式下也能解析到该目录,host 与 remote 的vite.config.ts都配置了server.fs.allow: ['.', '../shared']。这一组合体现了模块联邦项目中"跨应用共享代码"的两种手段:既可以在运行时通过联邦共享依赖,也可以在构建期通过共享目录共享源码模块。

验证运行结果与排查要点

启动npm run preview后,可以在浏览器中进行如下验证:

  1. 访问http://localhost:4173/,页面应同时渲染 host 卡片("I'm the host app")与远端卡片("I'm the remote app"),对应 README.md 中附图的效果;
  2. 打开开发者工具 Network 面板,应能看到对http://localhost:4174/remoteEntry.js的请求,以及随后按需拉取的 remote 模块分块;
  3. 点击两个 Counter 按钮,数字各自累加,证明两份组件实例与状态彼此独立;
  4. 在 Console 面板应能看到 host 打印的I'm RxJs from host与 remote 打印的Remote useEffect。

常见排查点:若远端组件持续显示 "loading...",优先确认 remote 的 preview 服务确实运行在 4174 端口,且 host 配置中entry的地址、name/entryGlobalName与 remote 端配置一致;若出现双 React 实例警告,检查两侧shared.react是否都设置了singleton: true且requiredVersion相互兼容。

小结:从本示例迁移到自有项目的三个步骤

基于 host/vite.config.ts 与 remote/vite.config.ts 的配置范式,可以总结出在 Vite 中搭建模块联邦应用的最小清单:

  1. remote 端:安装@module-federation/vite,在plugins中加入federation({ name, exposes, filename, shared }),把要共享的组件写进exposes;
  2. host 端:在federation({ name, remotes })中声明远端,remotes.<别名>的entry指向 remote 的remoteEntry.jsURL,name与entryGlobalName与 remote 的name保持一致;
  3. 运行时消费:在 host 组件中用lazy(() => import('别名/暴露键'))+Suspense包装远端组件,并通过shared.singleton确保 React 等关键依赖只存在一份。

示例本身固定使用localhost:4174作为远端入口地址,适用于本地演示与开发环境;若要在生产环境部署,需将remotes.remote.entry替换为实际托管的remoteEntry.js地址,并配合对应的跨域与缓存策略。更多 Vite 模块联邦的变体(Angular、Vue3、Solid、Svelte 等框架)可在仓库的module-federation-vite-*系列目录中横向对照参考。

  • 示例工程

【免费下载链接】module-federation-examples

Implementation examples of module federation , by the creators of module federation

项目地址:https://gitcode.com/gh_mirrors/mo/module-federation-examples
点击查看免费下载
上一篇:猫抓浏览器插件:免费网页资源嗅探与视频下载的终极解决方案
下一篇:DDNS 接入腾讯云 EdgeOne:加速域名源站 IP 动态更新配置指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进