Vite 模块联邦架构:跨项目共享组件的终极方案

📅 2026/7/15 21:39:11
Vite 模块联邦架构:跨项目共享组件的终极方案
Vite 模块联邦架构跨项目共享组件的终极方案大家好我是蔓蔓。最近在管理多个独立产品的前端项目时我发现同一个 Button 组件在三个仓库中各自维护了一个版本样式和交互行为已经开始产生细微的分化。在 Module Federation 2.0基于 Vite/Rspack成熟的当下这就是一个应该被彻底解决的基础设施问题。今天和大家分享基于 Vite 的模块联邦架构实践。一、多仓库时代的组件漂移共享成为工程化债务当一个团队同时维护多个前端项目时组件的共享通常会陷入三种困境困境分析方案优点弊端复制粘贴零学习成本组件版本分化、修复需要同步 N 次npm 私有包版本可追溯发布-安装链路长、调试困难、本地联调繁琐Git Submodule代码实时同步冲突处理复杂、CI 流程侵入性高Monorepo代码统一管理仓库体积膨胀、构建时间线性增长Module Federation 的优势定位Module Federation 提供了一个关键能力运行时动态加载远程模块。这意味着发布者更新组件后消费者无需重新构建部署即可获得最新版本共享依赖React、Vue、antd可在运行时去重避免重复加载多框架混合场景天然支持二、Vite Module Federation 的运行机制Vite 环境下Module Federation 由originjs/vite-plugin-federation插件驱动。其底层原理与 Webpack 5 的 Module Federation 一致但适配了 Vite 的 ESM 原生构建体系。sequenceDiagram participant Consumer as 消费者应用br/(Host) participant CDN as 静态资源 CDN participant Provider as 提供者应用br/(Remote) Consumer-CDN: 1. 加载 remoteEntry.jsbr/(仅包含模块映射表) CDN--Consumer: 返回模块清单 Consumer-Consumer: 2. 解析 import(remote/Button) Consumer-CDN: 3. 按需加载 Button 的 chunk CDN--Consumer: 返回组件代码 Consumer-Consumer: 4. 检查 shared scopebr/复用已有的 react/antd Consumer-Consumer: 5. 渲染组件提供者Remote配置// remote/vite.config.ts import { defineConfig } from vite; import react from vitejs/plugin-react; import federation from originjs/vite-plugin-federation; export default defineConfig({ plugins: [ react(), federation({ name: remote_components, filename: remoteEntry.js, exposes: { ./Button: ./src/components/Button/index.tsx, ./Table: ./src/components/Table/index.tsx, ./Form: ./src/components/Form/index.tsx, ./Theme: ./src/theme/index.ts, }, shared: { react: { singleton: true, requiredVersion: ^18.2.0, }, react-dom: { singleton: true, requiredVersion: ^18.2.0, }, antd: { singleton: true, }, }, }), ], build: { target: esnext, minify: false, // 生产环境应开启 cssCodeSplit: false, // 组件样式统一打包 }, });消费者Host配置// host/vite.config.ts import federation from originjs/vite-plugin-federation; export default defineConfig({ plugins: [ react(), federation({ name: host_app, remotes: { remote_components: { // 生产环境指向 CDN external: process.env.NODE_ENV production ? https://cdn.example.com/remote_components/assets/remoteEntry.js : http://localhost:5001/assets/remoteEntry.js, format: esm, from: vite, }, }, shared: { react: { singleton: true, requiredVersion: ^18.2.0 }, react-dom: { singleton: true, requiredVersion: ^18.2.0 }, antd: { singleton: true }, }, }), ], });三、生产级实践组件暴露策略与类型安全组件暴露的粒度决策不是所有组件都适合通过 Module Federation 暴露。推荐策略// remote/src/index.ts - 组件导出清单 // Tier 1: 基础原子组件高频共享稳定 API export { default as Button } from ./components/Button; export { default as Input } from ./components/Input; export { default as Select } from ./components/Select; // Tier 2: 业务组合组件中频共享API 偶有变化 export { default as UserSelector } from ./components/UserSelector; export { default as FileUploader } from ./components/FileUploader; // Tier 3: 不应暴露的页面级/强耦合业务逻辑 // export { default as OrderDetail } // 不应暴露跨项目类型安全Module Federation 的动态导入天然丢失类型信息。需要建立类型声明桥接// host/types/remote.d.ts declare module remote_components/Button { import { FC } from react; import type { ButtonProps as AntdButtonProps } from antd; interface RemoteButtonProps extends AntdButtonProps { variant?: primary | secondary | ghost; loading?: boolean; } const Button: FCRemoteButtonProps; export default Button; } declare module remote_components/Table { import { FC } from react; interface ColumnT { key: string; title: string; dataIndex: keyof T; render?: (value: T[keyof T], record: T) React.ReactNode; } interface TablePropsT { columns: ColumnT[]; dataSource: T[]; loading?: boolean; onRowClick?: (record: T) void; } const Table: T extends Recordstring, unknown( props: TablePropsT ) React.ReactElement; export default Table; }远程组件的错误边界远程模块加载失败时必须有优雅降级方案import { Suspense, lazy, Component, type ReactNode } from react; class RemoteErrorBoundary extends Component { fallback: ReactNode; children: ReactNode }, { hasError: boolean } { state { hasError: false }; static getDerivedStateFromError() { return { hasError: true }; } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } } // 带降级的远程组件加载器 function createRemoteComponentT( importFn: () Promise{ default: T }, FallbackComponent: React.FC ) { const LazyComponent lazy(importFn); return function RemoteComponent(props: any) { return ( RemoteErrorBoundary fallback{FallbackComponent /} Suspense fallback{divLoading remote component.../div} LazyComponent {...props} / /Suspense /RemoteErrorBoundary ); }; } // 使用 const RemoteButton createRemoteComponent( () import(remote_components/Button), () button disabled组件加载失败/button );样式隔离方案// remote/src/components/Button/index.tsx // 使用 CSS Module 避免样式冲突 import styles from ./index.module.css; export default function Button({ variant primary, ...props }: Props) { return ( button className{${styles.btn} ${styles[variant]}} {...props} / ); }踩坑共享依赖版本不一致导致的运行时崩溃Module Federation 的shared配置中最危险的陷阱是版本不兼容。在开发环境中一切正常但生产环境可能出现以下场景Host 应用依赖react18.2.0Remote 组件也声明react: { requiredVersion: ^18.2.0 }。看起来配置没问题但如果 Host 安装了react18.3.1自动升级而 Remote 的singleton: true强制使用 Host 提供的版本此时 Remote 中依赖react18.2.0内部 API 的组件可能在新版本下行为变化甚至直接崩溃。更严重的情况是未声明 singleton 的依赖重复加载。比如两个应用各自打包了一份antd页面同时渲染 Host 和 Remote 的 Button 时两份 antd 的 ConfigProvider 互相冲突主题配置失效。解决方案所有共享依赖必须声明singleton: true并使用精确的版本范围。在 CI 中增加依赖版本一致性检查脚本// scripts/check-shared-deps.ts function checkSharedDependencyConsistency( hostConfig: FederationConfig, remoteConfig: FederationConfig ): string[] { const warnings: string[] []; for (const [dep, hostOpts] of Object.entries(hostConfig.shared || {})) { const remoteOpts remoteConfig.shared?.[dep]; if (!remoteOpts) { warnings.push(Remote 未声明共享依赖 ${dep}可能导致重复加载); continue; } if (!hostOpts.singleton || !remoteOpts.singleton) { warnings.push(${dep} 未设置 singleton: true可能加载多份实例); } if (hostOpts.requiredVersion ! remoteOpts.requiredVersion) { warnings.push(${dep} 版本要求不一致: Host${hostOpts.requiredVersion}, Remote${remoteOpts.requiredVersion}); } } return warnings; }四、生产部署与版本管理策略部署流程提供者构建 → 上传到 CDN → 更新版本映射表 ↓ 消费者应用 → 下次用户访问时自动拉取最新 remoteEntry.js关键点消费者无需重新构建但需要在remoteEntry.js上设置合理的缓存策略。# CDN 缓存配置 location /remoteEntry.js { add_header Cache-Control public, max-age300, must-revalidate; # 5 分钟缓存确保版本更新能及时生效 } location /assets/ { add_header Cache-Control public, max-age31536000, immutable; # 带 hash 的静态资源可以长期缓存 }灰度发布策略// host/src/remotes.ts function getRemoteUrl( name: string, version?: string ): string { const base https://cdn.example.com/remote_components; // 灰度用户使用新版本 if (version) { return ${base}${version}/assets/remoteEntry.js; } return ${base}/assets/remoteEntry.js; } // 通过 Cookie 或 Header 控制灰度 const isGrayUser document.cookie.includes(remote_versionv2); const remoteUrl getRemoteUrl( remote_components, isGrayUser ? 2.0.0 : undefined );五、总结Vite 模块联邦为跨项目组件共享提供了一条不同于 npm 包和 Monorepo 的路径。三个核心决策点第一远程组件的粒度决定了架构的健康度。暴露过多会导致消费者耦合过深暴露过少则失去共享的意义。基础组件Button/Input/Select和高度稳定的业务组件是最佳候选。第二类型声明是长期可维护性的基石。没有类型声明的远程组件会在迭代中不断积累隐性契约最终导致消费者不敢升级。类型声明文件应当与组件代码放在同一仓库中维护。第三缓存策略直接影响更新生效速度。remoteEntry.js的缓存时间越短版本更新生效越快但 CDN 回源压力越大。5 分钟的缓存窗口是一个经过验证的平衡点。推荐落地路径先从 2-3 个使用频率最高的基础组件开始试点验证构建、部署、消费全流程的顺畅性再逐步扩大暴露范围。你在项目中是如何解决跨仓库组件共享问题的欢迎在评论区交流