模块联邦

运行时远程模块加载与共享机制
Webpack 5
联邦模块标准实现
<50ms
热更新延迟
CDN
远程模块加载方式
Shared
跨应用依赖复用

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。

样式隔离

Shadow DOM、CSS Modules 与命名空间策略
Shadow DOM
最强隔离级别
BEM
命名约定隔离
CSS-in-JS
运行时隔离
PostCSS
构建时隔离

样式冲突根源

微前端场景下样式冲突是最常见的问题。不同子应用可能使用相同的 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 存储跨应用状态

共享依赖

公共库版本治理与运行时复用
30-50%
共享依赖体积缩减
Singleton
单例共享模式
Dedupe
依赖去重策略
Flat
版本平铺策略

依赖共享的必要性

如果每个子应用都独立打包 React、React-DOM、Router、State Management 等库,最终用户需要下载多份相同的代码,造成带宽浪费与解析重复。shared 配置允许跨应用共享同一份依赖实例。

  • Singleton 共享:同一库的多个版本仅保留一个,强制版本一致(如 react)
  • 版本范围匹配:Host 与 Remote 的版本范围取交集,超出交集则各自打包
  • Eager 共享:同步加载的共享库,无需等待异步加载,适合 UI 组件库
// shared 配置详解
shared: {
    // Singleton: 仅允许一个版本存在(React、React-DOM 必须)
    react: {
        singleton: true,
        requiredVersion: '^18.0.0',
        strictVersion: true,  // 严格模式,版本不符时报错
    },
    'react-dom': {
        singleton: true,
        requiredVersion: '^18.0.0',
    },
    // 非 Singleton:允许不同版本共存(工具库)
    lodash: {
        singleton: false,
        requiredVersion: '^4.17.0',
    },
    // eager: true 跳过异步加载,直接同步打包
    // 适用:小体积、高频使用、无条件共享
    dayjs: {
        eager: true,
        // 但会增加主 bundle 体积,需权衡
    }
}

// 版本冲突时的加载顺序(Flat)
// Webpack 5 会"扁平化"依赖树,选择最接近的版本
// Host: react@18.2.0, Remote: react@18.0.0 → 使用 18.2.0

版本碎片化风险

过度追求共享可能导致子应用被迫使用旧版共享库,错过安全更新和性能优化。建议对共享库版本做年度审计和强制升级。

共享 UI 组件库

推荐做法:主应用维护一套共享的 Design System 组件(如 Button、Input、Modal),子应用通过 Remote 引用。避免各子应用自建重复组件。

本地开发策略

子应用开发时使用 npm link 或 yarn workspace 链接本地 shared 库;CI 阶段使用构建后的 CDN 版本做集成测试。

架构决策矩阵

微前端技术选型需考虑:团队规模(超过 20 人时分拆价值明显)、应用复杂度(业务边界清晰时分拆成本低)、发布频率(各团队独立发布频率差异大时价值高)。对于中小型应用(5人以下团队),微前端的协调成本可能超过收益。