模块联邦
Module Federation 核心原理
Module Federation(MF)是 Webpack 5 引入的运行时共享机制,允许一个应用的 bundle 动态加载另一个应用暴露的模块。与传统 iframe 方案相比,MF 保持了同源环境的上下文共享,JS 状态可跨应用传递。
- Host(主机应用):作为容器,负责动态下载并执行远程模块的代码
- Remote(远程应用):独立构建部署,将自身模块注册到容器的模块系统
- Bidirectional(双向宿主):应用既可作为 Host 加载他人,也可作为 Remote 被他人加载
- shared:配置共享的第三方库(如 React、Vue),避免重复打包
// Webpack 5 Module Federation 配置 // remote-app/webpack.config.js const { ModuleFederationPlugin } = require('webpack').container; module.exports = { plugins: [ new ModuleFederationPlugin({ // 应用名称,用于其他应用引用 name: 'remoteApp', filename: 'remoteEntry.js', // 入口文件名称 exposes: { // 暴露的组件路径映射 './Button': './src/components/Button', './ProductCard': './src/components/ProductCard', }, shared: { // 共享的依赖,减少重复打包 react: { singleton: true, requiredVersion: '^18.0.0' }, 'react-dom': { singleton: true }, } }) ] }; // host-app/webpack.config.js module.exports = { plugins: [ new ModuleFederationPlugin({ name: 'hostApp', remotes: { // 引用远程应用 'remoteApp': 'remoteApp@https://remote.example.com/remoteEntry.js', }, shared: { react: { singleton: true }, 'react-dom': { singleton: true }, } }) ] };
// Host 应用中动态加载远程组件 // App.tsx import { lazy, Suspense } from 'react'; // 动态导入远程 Button 组件 const RemoteButton = lazy(() => import('remoteApp/Button')); const RemoteProductCard = lazy(() => import('remoteApp/ProductCard')); function App() { return ( <div> {/* Suspense 处理加载状态 */} <Suspense fallback={<Loading />}> {/* 远程组件使用方式与本地组件完全一致 */} <RemoteButton label="Submit" onClick={() => {}} /> <RemoteProductCard product={productData} /> </Suspense> </div> ); } // React 18 的 use() hook 也可与联邦模块配合 // import { use } from 'react'; // const buttonModule = use(import('remoteApp/Button'));
版本解耦
每个子应用独立构建、版本独立演进。Host 可锁定远程应用的特定版本,或配置 fallback 策略处理加载失败。
共享作用域
Host 与 Remote 在同一 JS 上下文中运行,共享 window 对象、localStorage、cookie 等浏览器 API,跨应用状态传递无需额外序列化。
动态共享
shared 配置允许选择性共享库,也可配置 eager: true 跳过异步加载直接同步使用,但会增加主 bundle 体积。
独立部署
部署架构设计
微前端的核心价值在于团队自治与独立发布。每个子应用拥有独立的 Git 仓库、独立 CI/CD 流水线、独立 CDN 部署,最终在运行时由 Host 应用动态组装。
- 独立仓库策略:每个子应用独立仓库,代码隔离、权限隔离、依赖版本隔离。但需协调公共组件的 API 契约。
- 渐进式迁移:遗留巨石应用(Monolith)通过功能抽取逐步迁移,每次迁移一个功能模块,不影响整体运行。
- Feature Toggle:通过配置中心控制子应用的可见性,新功能可先灰度后全量,降低发布风险。
// 部署配置示例(使用 CDN + 远程入口) // Host 应用的部署配置中维护远程映射 const RemoteConfig = { remoteApp: { url: 'https://cdn.example.com/remote-app/@1.2.3/remoteEntry.js', fallback: 'https://cdn.example.com/remote-app/@1.2.2/remoteEntry.js', env: 'production', }, ordersApp: { url: 'https://cdn.example.com/orders-app/@2.0.1/remoteEntry.js', fallback: null, // 无 fallback时报错 env: 'production', } }; // CI/CD Pipeline 伪代码(GitHub Actions / Jenkins) // .github/workflows/deploy.yml /* name: Deploy Remote App on: push: branches: [main] jobs: build-and-deploy: steps: - uses: actions/checkout@v3 # 子应用独立构建 - name: Build run: npm ci && npm run build:mf # 上传到 CDN - name: Upload to CDN run: | VERSION=\${GITHUB_SHA:0:7} aws s3 sync dist/ s3://cdn.example.com/remote-app/@\${VERSION}/ # 更新版本映射(DNS 或 配置中心) - name: Register Version run: | register-version --app=remoteApp --version=\${VERSION} \ --url=s3://cdn.example.com/remote-app/@\${VERSION}/remoteEntry.js */
版本兼容策略
Remote 应用的 API 变更应遵循语义化版本(SemVer)。主版本变更意味着 Breaking Change,Host 必须同步更新才可继续使用。子版本和补丁版本应保持向后兼容。
- Contract Test:在 CI 中验证暴露的 API 契约未破坏
- 灰度回滚:先切 5% 流量,新版本异常则快速回退
- Stub 模式:开发阶段使用本地 Mock,Remote 未部署也可开发
团队边界划分
微前端的最大挑战是团队间协调成本。推荐按业务域(Domain)而非技术层划分团队,每个团队端到端负责一个业务域(前端 + 后端 + DBA)。
- 订单团队:负责下单、支付、退款流程
- 商品团队:负责商品搜索、详情、推荐
- 用户团队:负责登录、账户、权益
微前端并非银弹。过度碎分的子应用会引发:CSS 命名冲突加剧、用户首次加载资源数爆炸(HTTP/2 缓解但非消除)、跨应用调试链路断裂。推荐子应用数量控制在 10 个以内,业务边界清晰且独立度高时才引入 MF。
样式隔离
样式冲突根源
微前端场景下样式冲突是最常见的问题。不同子应用可能使用相同的 CSS 类名(如 .button、.modal),导致样式互相覆盖。根本原因是 CSS 全局作用域的特性——所有样式默认应用于整个文档树。
- 第三方库污染:UI 库(Bootstrap、Ant Design)会注入全局样式
- 团队命名重叠:不同团队独立开发,使用相同语义化类名
- Reset 样式冲突:不同的 CSS Reset 相互覆盖
// 方案1:Shadow DOM 隔离(最强但有局限) // 使用 Web Component 的 Shadow DOM 包裹子应用容器 class MicroAppContainer extends HTMLElement { connectedCallback() { const shadow = this.attachShadow({ mode: 'open' }); const wrapper = document.createElement('div'); wrapper.id = 'micro-app-root'; shadow.appendChild(wrapper); // 子应用的样式在 Shadow DOM 内有效,无法穿透 const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = './子应用样式.css'; shadow.appendChild(link); } } customElements.define('micro-app-container', MicroAppContainer); // 局限性:Shadow DOM 内部的 JS 无法直接访问父文档 DOM, // 需要通过事件通信(CustomEvent)跨边界传递消息
// 方案2:CSS Modules(推荐)—— 构建时隔离 // Button.module.css(子应用内) /* 生成唯一的哈希后缀类名 */ .button { /* → .button_abc123 */ padding: 8px 16px; border-radius: 4px; } .buttonPrimary { /* → .buttonPrimary_xyz789 */ background: blue; } // Button.tsx 使用 import styles from './Button.module.css'; // styles.button === 'button_abc123' export const Button = ({ variant }) => ( <button className={variant === 'primary' ? styles.buttonPrimary : styles.button}> Click me </button> ); // 方案3:CSS Custom Properties 命名空间 /* 子应用根元素上添加 data-namespace 属性 */ /* <div data-namespace="orders-app"> */ [data-namespace="orders-app"] .button { /* 仅对 orders-app 内的 .button 生效 */ background: var(--orders-button-bg, #4d7cff); }
CSS Custom Properties
通过 CSS 变量定义设计令牌(Design Token),子应用引用时使用自己的命名空间变量。即使引入相同的 class 名,只要变量映射正确就不会冲突。
PostCSS Modules
构建时通过 PostCSS 插件将所有类名转换为带哈希后缀的唯一类名,类似 CSS Modules 但不强制使用 .module.css 文件。
Style Tag 注入
子应用挂载时在根元素内注入 style 标签,所有样式规则自动限定在子应用 DOM 子树下,无需特殊工具。
路由编排
路由架构模型
微前端的路由分为两种模式:中心化路由(主应用控制所有路由,子应用仅负责渲染)和分布式路由(子应用各自注册自身前缀路由)。
- 中心化路由:主应用维护全局路由表,通过路径前缀将请求分发到对应子应用。子应用作为纯渲染层,不感知路由。
- 分布式路由:子应用注册自身路由前缀,URL 匹配由各自子应用自行处理。适合子应用间无强耦合的场景。
// 中心化路由示例(Single SPA 模式) // 主应用路由配置 import { registerApplication, start } from 'single-spa'; registerApplication({ name: 'orders', app: () => import('http://cdn.example.com/orders.js'), activeWhen: ['/orders', '/checkout', '/cart'], // 自定义 props 传递给子应用 customProps: { authToken: getAuthToken(), } }); registerApplication({ name: 'products', app: () => import('http://cdn.example.com/products.js'), activeWhen: ['/products', '/search'], }); // 启动主应用 start({ urlRerouteOnly: true, }); // 子应用(orders)导出生命周期函数 // src/bootstrap.js export function bootstrap(props) { // 初始化逻辑 } export function mount(props) { // 挂载到 DOM ReactDOM.render(<App />, props.domElement); } export function unmount(props) { // 卸载,清理副作用 }
嵌套路由与 404 处理
子应用可能需要处理嵌套路由(如 /orders/:orderId/details)。中心化路由需要在主应用层做精确的前缀匹配,避免过度匹配导致子应用收到不属于自己的路由。
- 主应用:/orders → orders 子应用,/orders/* → orders 子应用
- 子应用:自主处理 /123、/456 等具体路由
- 404:未匹配到任何子应用的路径,由主应用兜底处理
URL 同步与状态管理
子应用切换时浏览器 URL 需要正确更新,Browser History API 与 single-spa 的 Navigation API 需协调。避免子应用直接操作 history.pushState,应通过主应用协调防止路由冲突。
- 使用 history.listen 监听路由变化
- 子应用状态变更通过 event bus 同步到主应用
- 共享 sessionStorage 存储跨应用状态