微前端与模块联邦:架构设计与落地实战

系统性微前端技术指南:微前端架构模式(Module Federation / iframe / Web Components / qiankun / single-spa)选型对比、Webpack Module Federation 原理与配置、Vite Module Federation(@originjs/vite-plugin-federation)实战、公共依赖共享与版本冲突解决、跨应用通信、路由整合、样式隔离、qiankun 沙箱与快速启动、微前端 DevOps 与独立部署。附完整架构决策树。

微前端不是「解决所有问题的银弹」,而是「在巨石应用(Monolith)崩溃前的架构解药」。 当团队规模超过 50 人、技术栈需要渐进式升级、或需要独立部署多个业务线时,微前端值得考虑。但它的代价是额外的复杂度和运行时开销,需审慎评估。


一、为什么需要微前端

1.1 巨石应用的问题

单一仓库单体应用(Monolith)的痛点:
├── 构建时间 10 分钟+ → 开发体验差
├── 任何小改动需要全量回归测试
├── 技术栈锁定(无法渐进式升级 Vue2→Vue3)
├── 多个业务线团队互相阻塞代码合并
├── 发布窗口冲突(A 团队想发,B 团队还没测完)
└── 代码库 50 万行+,新人上手周期 2 个月

1.2 微前端的核心价值

价值说明
技术栈无关子应用可用不同框架(Vue2/Vue3/React/Angular)
独立部署每个子应用有自己的 CI/CD 流水线
团队自治业务线团队独立开发、测试、发布
渐进式重构老系统逐步迁移,而非重写
故障隔离单个应用崩溃不拖垮整个系统

二、微前端架构模式对比

2.1 四种主要方案

方案原理优点缺点代表
Module Federation运行时模块共享真正的 JS 共享、依赖去重构建配置复杂Webpack 5 / Vite
iframe独立文档上下文完美隔离、简单路由同步难、弹窗遮罩问题、体验差传统方案
Web Components标准组件化原生标准、框架无关生态不成熟、通信复杂Lit / Stencil
JS 沙箱(qiankun)动态加载 + 沙箱隔离配置简单、生态成熟基于 eval,性能一般qiankun / single-spa

2.2 选型决策树

需要微前端吗?
├── 团队 < 20 人 + 单一技术栈?
│   └── 是 → 不需要微前端,用 Monorepo 即可
│   └── 否 → 继续
├── 必须多技术栈共存(Vue2 + Vue3 + React)?
│   └── 是 → qiankun / single-spa
│   └── 否 → 继续
├── 追求极致运行时性能 + 模块共享?
│   └── 是 → Module Federation(Webpack 5 / Vite)
│   └── 否 → 继续
├── 需要最简单配置 + 快速上线?
│   └── 是 → qiankun(国内生态最好)
│   └── 否 → single-spa
└── 长期看好 Web 标准?
    └── 是 → Web Components(长期但当前生态弱)

三、Module Federation 深度解析

3.1 核心概念

Module Federation 角色:
├── Host(基座/入口应用)
│   └── 消费远程模块,负责路由整合、公共布局
├── Remote(子应用/远程模块)
│   └── 暴露自己的组件/页面,被 Host 消费
└── Shared(共享依赖)
    └── React/Vue 等库只加载一次,所有应用共用

3.2 Webpack 5 配置

// 基座应用(Host)webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        // 远程应用名: '远程应用名@远程入口URL'
        dashboard: 'dashboard@https://dashboard.example.com/remoteEntry.js',
        settings: 'settings@https://settings.example.com/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        vue: { singleton: true, requiredVersion: '^3.4.0' },
      },
    }),
  ],
};
// 子应用(Remote)webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'dashboard',
      filename: 'remoteEntry.js', // 暴露的入口文件
      exposes: {
        './App': './src/App',                    // 暴露整个应用
        './Widget': './src/components/Widget',   // 暴露单个组件
        './utils': './src/utils',                // 暴露工具模块
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true },
      },
    }),
  ],
};
// 基座消费远程模块
import { lazy, Suspense } from 'react';

// 动态导入远程模块
const DashboardApp = lazy(() => import('dashboard/App'));
const SettingsApp = lazy(() => import('settings/App'));

function Layout() {
  return (
    <div className="layout">
      <nav>{/* 全局导航 */}</nav>
      <main>
        <Suspense fallback={<div>Loading...</div>}>
          <Routes>
            <Route path="/dashboard/*" element={<DashboardApp />} />
            <Route path="/settings/*" element={<SettingsApp />} />
          </Routes>
        </Suspense>
      </main>
    </div>
  );
}

3.3 Vite Module Federation

npm install -D @originjs/vite-plugin-federation
// Host vite.config.ts
import federation from '@originjs/vite-plugin-federation';

export default {
  plugins: [
    federation({
      name: 'host',
      remotes: {
        remote_app: 'http://localhost:3001/assets/remoteEntry.js',
      },
      shared: ['vue', 'vue-router', 'pinia']
    })
  ],
  build: {
    target: 'esnext', // 必须指定
    minify: false,
    cssCodeSplit: false
  }
};
// Remote vite.config.ts
import federation from '@originjs/vite-plugin-federation';

export default {
  plugins: [
    federation({
      name: 'remote_app',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button.vue',
        './App': './src/App.vue'
      },
      shared: ['vue', 'vue-router']
    })
  ],
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: true
  }
};

四、qiankun 实战

4.1 基座配置

// main/src/main.ts(Vue 3 基座)
import { createApp } from 'vue';
import { registerMicroApps, start } from 'qiankun';
import App from './App.vue';
import router from './router';

const app = createApp(App);
app.use(router).mount('#app');

// 注册微应用
registerMicroApps([
  {
    name: 'vue3-app',
    entry: '//localhost:3001',
    container: '#micro-app-container',
    activeRule: '/vue3',
    props: { brand: 'MyCompany' }, // 传递数据给子应用
  },
  {
    name: 'react-app',
    entry: '//localhost:3002',
    container: '#micro-app-container',
    activeRule: '/react',
  },
  {
    name: 'legacy-app', // 甚至可以是纯 HTML
    entry: '//localhost:3003',
    container: '#micro-app-container',
    activeRule: '/legacy',
  },
], {
  // 生命周期钩子
  beforeLoad: (app) => console.log('before load', app.name),
  afterMount: (app) => console.log('after mount', app.name),
});

start({
  sandbox: {
    strictStyleIsolation: true,   // Shadow DOM 样式隔离
    experimentalStyleIsolation: true // Scoped CSS 样式隔离
  },
  prefetch: 'all', // 预加载所有子应用
});

4.2 子应用改造(Vue 3 示例)

// vue3-app/src/main.ts
import { createApp } from 'vue';
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper';
import App from './App.vue';
import router from './router';

let app: ReturnType<typeof createApp>;

function render(props: any = {}) {
  const { container } = props;
  app = createApp(App);
  app.use(router);
  app.mount(container ? container.querySelector('#app') : '#app');
}

// 独立运行
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
  render();
}

// qiankun 生命周期
renderWithQiankun({
  mount(props) {
    render(props);
    console.log('vue3-app mounted', props);
  },
  bootstrap() {
    console.log('vue3-app bootstrap');
  },
  unmount() {
    app.unmount();
  },
  update(props) {
    console.log('vue3-app update', props);
  }
});
// vue3-app/vite.config.ts
import qiankun from 'vite-plugin-qiankun';

export default {
  base: '/vue3/', // 子应用前缀
  plugins: [
    qiankun('vue3-app', { useDevMode: true })
  ],
  server: {
    port: 3001,
    headers: {
      'Access-Control-Allow-Origin': '*' // qiankun fetch 需要 CORS
    }
  }
};

五、样式隔离方案

5.1 方案对比

方案实现优点缺点
CSS Modules编译时 hash零运行时开销需要改造
Scoped CSS(Vue)属性选择器Vue 项目原生仅 Vue
Shadow DOMWeb 标准隔离最强隔离qiankun strictStyleIsolation
CSS-in-JSJS 运行时 hash自动隔离运行时开销
命名前缀约定BEM / 团队规范简单靠纪律
PostCSS 前缀插件自动加前缀自动化需构建配置

5.2 推荐组合

微前端样式隔离策略:
├── 基座 + 子应用全部 Vue → Scoped CSS / CSS Modules
├── 基座 React + 子应用混合 → CSS Modules + CSS-in-JS
├── 强隔离需求 → qiankun Shadow DOM(strictStyleIsolation)
└── 全局样式冲突(如 Ant Design + Element Plus)→ CSS Modules + 全局命名空间

六、跨应用通信

6.1 方案选择

方案适合示例
Props 向下传递基座→子应用qiankun 的 props
自定义 Events子应用→基座window.dispatchEvent
全局状态共享频繁双向通信共享 Redux / Pinia / Zustand
URL 参数状态可分享路由 query params

6.2 基于 EventBus 的通信

// shared-utils/eventBus.ts
class EventBus {
  private events: Map<string, Set<Function>> = new Map();

  on(event: string, handler: Function) {
    if (!this.events.has(event)) this.events.set(event, new Set());
    this.events.get(event)!.add(handler);
  }

  off(event: string, handler: Function) {
    this.events.get(event)?.delete(handler);
  }

  emit(event: string, data?: any) {
    this.events.get(event)?.forEach(fn => fn(data));
  }
}

export const microEventBus = new EventBus();

// 基座使用
import { microEventBus } from '@my-org/shared-utils';
microEventBus.on('user:login', (user) => {
  console.log('User logged in:', user);
});

// 子应用使用
microEventBus.emit('user:login', { id: 'u1', name: 'Alice' });

6.3 共享状态(推荐)

// shared-utils/globalStore.ts
import { reactive, readonly } from 'vue';

const state = reactive({
  user: null as User | null,
  theme: 'light' as 'light' | 'dark',
  notifications: [] as Notification[],
});

export const globalStore = {
  state: readonly(state),
  setUser(user: User) { state.user = user; },
  setTheme(theme: 'light' | 'dark') { state.theme = theme; },
  addNotification(n: Notification) { state.notifications.push(n); }
};

// 所有应用引用同一实例(通过 shared deps 或 window 挂载)

七、DevOps 与独立部署

7.1 独立部署架构

                    ┌─────────────┐
                    │   CDN / Nginx │
                    │   (统一入口)  │
                    └──────┬──────┘
              ┌────────────┼────────────┐
              ↓            ↓            ↓
         ┌────────┐  ┌────────┐  ┌────────┐
         │ 基座   │  │ 子应用A │  │ 子应用B │
         │ Host   │  │ Remote │  │ Remote │
         └────────┘  └────────┘  └────────┘
              ↑            ↑            ↑
              └────────────┼────────────┘
                    ┌──────┴──────┐
                    │ 各自 CI/CD  │
                    └─────────────┘

基座部署 → 更新路由表、公共布局
子应用A 部署 → 仅更新 //app-a.example.com/remoteEntry.js
子应用B 部署 → 仅更新 //app-b.example.com/remoteEntry.js

7.2 CI/CD 流水线(GitHub Actions)

# 子应用 A: .github/workflows/deploy.yml
name: Deploy Micro App A
on:
  push:
    branches: [main]
    paths: ['apps/app-a/**']

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - run: pnpm install
      - run: pnpm --filter app-a build
      - name: Deploy to CDN
        run: |
          aws s3 sync apps/app-a/dist s3://my-cdn/app-a/${{ github.sha }}/
          aws s3 cp apps/app-a/dist/remoteEntry.js s3://my-cdn/app-a/latest/

八、微前端 Checklist

检查项状态说明
确定是否真的需要微前端团队>30人?多技术栈?独立部署需求?
选定架构方案Module Federation / qiankun / iframe
公共依赖共享React/Vue singleton,版本兼容
样式隔离Shadow DOM / CSS Modules / Scoped
路由整合基座路由 + 子应用路由不冲突
跨应用通信Props / EventBus / 共享状态
错误边界子应用崩溃不影响基座
加载策略prefetch / lazy / 按需加载
独立部署验证子应用单独发布,基座无感知
监控每个子应用独立监控 + 基座汇总

参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. API 设计与 BFF 层:REST、GraphQL、tRPC 选型与前后端协作
  2. WebAssembly 前端工程化实践:编译链、性能对比与混合架构
  3. 现代浏览器 API 与 Web 平台能力地图