CC 咖啡猫的工作空间 Coding Space

前端微前端与模块联邦

微前端是将前端应用拆分为多个独立开发、独立部署、独立运行的小型应用,通过组合方式构成一个完整产品。本文从实战角度出发,系统性讲解微前端概念、方案选型、Module Federation 与 Qiankun 实践,以及工程化要点。


一、微前端核心概念

1.1 什么是微前端

微前端 (Micro Frontends) 是指将前端应用拆分为多个功能独立的子应用,每个子应用可由不同团队使用不同技术栈独立开发、测试、部署,最终在主应用(基座)中统一组合呈现。

                    ┌─────────────────────────────────┐
                    │        主应用(基座容器)           │
                    ├──────────┬──────────┬───────────┤
                    │          │          │           │
                    ▼          ▼          ▼           ▼
              ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────┐
              │  导航   │ │  商品  │ │  订单  │ │   用户    │
              │ (团队A) │ │ (团队B) │ │ (团队C) │ │ (团队D)  │
              │ Vue 3   │ │ React  │ │ Vue 2  │ │ Angular  │
              └────────┘ └────────┘ └────────┘ └──────────┘

1.2 为什么需要微前端

痛点 单体前端表现 微前端解决
代码规模爆炸 单仓库百万行代码,构建耗时 > 10 分钟 子应用独立构建,单应用 < 30 秒
团队协作冲突 多人修改同一仓库,git merge 频繁冲突 子应用独立仓库,互不干扰
技术栈锁定 一荣俱荣,一损俱损,升级框架成本极高 子应用自主选型,渐进升级
独立发布困难 改一行代码需重新部署整个应用 子应用独立部署,灰度发布
存量系统迁移 老系统重构需要一次性替换 新功能用新技术栈,逐步替换老系统

1.3 与后端微服务的类比

维度 后端微服务 前端微前端
拆分粒度 按业务领域拆分服务 按业务域/页面拆分子应用
通信方式 HTTP/RPC/消息队列 自定义事件/共享状态/路由参数
数据隔离 数据库独立 样式/JS沙箱/Store 隔离
部署方式 独立部署、独立扩缩容 独立构建、独立部署(CDN)
技术异构 不同语言(Java/Go/Python) 不同框架(Vue/React/Angular)
治理手段 注册中心/配置中心/网关 基座应用/沙箱/路由分发

1.4 适用场景判断矩阵

不适用微前端的情况:

  • 团队 1~5 人,应用简单(CRUD 后台管理)
  • 技术栈统一且短期内不会变更
  • 没有独立发布的需求
  • 项目周期短、交付时间紧

适用微前端的情况:

场景 说明 决策
多团队协作大型应用 5+ 个团队开发同一产品,各自负责独立模块 ✅ 强烈推荐
存量系统渐进迁移 旧系统 jQuery/Vue2 -> 新系统 Vue3/React ✅ 推荐使用
中台/平台型产品 多个业务线共享同一个 Portal ✅ 推荐使用
第三方插件/扩展体系 允许第三方开发独立模块挂载到主应用 ✅ 必须使用
简单后台管理系统 2~3 人团队维护,1 个 repo ❌ 不推荐(引入复杂度 > 收益)
独立 H5 活动页 一次性营销活动,单页应用 ❌ 完全不需要

二、微前端方案对比

2.1 主流方案速览

方案 出品方 核心原理 技术栈适配 成熟度 GitHub Stars
Qiankun 阿里 基于 single-spa,HTML Entry + Proxy 沙箱 任意框架 16k+
Micro-app 京东 WebComponent + CustomEvent,类 iframe 思想 任意框架 5k+
Wujie 腾讯 iframe + WebComponent,WebComponent 做 UI 挂载 任意框架 4k+
Module Federation Webpack 官方 运行时模块加载 + 依赖共享 需 Webpack 5 / Vite 插件 Webpack 官方
single-spa 社区 路由分发 + 生命周期管理 任意框架 13k+

2.2 方案对比表

维度 Qiankun Micro-app Wujie Module Federation
沙箱机制 ✅ Proxy 沙箱 + Script 劫持 ✅ WebComponent + Proxy 沙箱 ✅ iframe 天然隔离 + Proxy ❌ 无沙箱(依赖 shared 隔离)
样式隔离 ✅ StrictStyleIsolation / Scoped CSS ✅ Shadow DOM / Scoped ✅ 天然隔离(iframe 天然自带) ❌ CSS Modules / CSS-in-JS 需自建
子应用改造成本 ⚠️ 需改造 webpack 导出生命周期 ✅ 几乎零改造 ✅ 几乎零改造 ✅ 导出 expose 配置即可
通信机制 initGlobalState + props CustomEvent + data window.parent 通信 shared 模块 + 自定义事件
性能开销 中(Proxy 沙箱有一定损耗) 中(WebComponent 额外 DOM) 低(iframe 复用,DOM 在 Shadow 中) 最低(纯模块加载)
跨技术栈 ✅ 支持 ✅ 支持 ✅ 支持 ⚠️ 需 shared 暴露统一接口
SSR 支持 ❌ 不支持 ❌ 不支持 ❌ 不支持 ✅ 支持 Module Federation SSR
构建工具 webpack(需插件) webpack(需插件) webpack(需插件) Webpack 5 / Vite(插件)
运行时 路由劫持 自定义路由 iframe 桥接 模块运行时
首屏加载 慢(需加载 HTML Entry) 最快(iframe 预加载) 快(按需加载 remote 模块)
坑点 内存泄漏排查困难;子应用频繁切换加载慢 Shadow DOM 下某些 UI 库样式异常 iframe 焦点/键盘事件问题 无沙箱,子应用需严格约定

2.3 选型建议

graph TD
    A[选择微前端方案] --> B{是否需要彻底隔离?}
    B -->|是| C{需要跨技术栈?}
    B -->|否| D{技术栈统一?}
    C -->|是| E[Wujie / Micro-app]
    C -->|否| F[Qiankun]
    D -->|是| G[Module Federation]
    D -->|否| E
团队情况 推荐方案 理由
阿里系技术栈,已有 single-spa 经验 Qiankun 生态完善,文档丰富
需要最小改造成本接入 Micro-app 几乎零改造,类 iframe 设计
极致隔离要求(如第三方插件市场) Wujie iframe 天然隔离,安全可靠
技术栈统一(全 Vue3 或全 React),追求性能 Module Federation 零沙箱开销,native 模块加载
已有 Webpack 5 项目,快速验证 Module Federation 零额外依赖,仅配置即可

三、Module Federation 深度实践

3.1 原理说明

Module Federation(模块联邦)是 Webpack 5 的核心特性,允许多个独立构建的应用在运行时动态加载彼此模块

┌─────────────────────┐    运行时远程加载     ┌─────────────────────┐
│  Host(主应用)      │ ◄────────────────    │  Remote(远程应用)  │
│  ┌────────────────┐ │                      │  ┌────────────────┐ │
│  │ import('remote/ │ │                      │  │ exposes: {     │ │
│  │  ./Button')     │ │                      │  │  './Button'   │ │
│  └────────────────┘ │                      │  └────────────────┘ │
│  shared: {          │                      │  shared: {         │
│    react,           │                      │    react,          │
│    react-dom        │                      │    react-dom       │
│  }                  │                      │  }                 │
└─────────────────────┘                      └─────────────────────┘

核心原理:

  1. exposes:远程应用暴露给外部使用的模块
  2. remotes:主应用声明需要从远程应用加载的模块
  3. shared:声明共享依赖,避免重复打包(如 react/react-dom/Vue)
  4. 运行时加载import('remote_app/Module') 在运行时发起 JSONP 请求,拉取远程应用的构建产物(remoteEntry.js)

3.2 Webpack Module Federation 配置

3.2.1 远程应用(Remote)—— 暴露模块

Vue 3 子应用(组件暴露):

// webpack.config.ts(Vue 3 子应用)
import { ModuleFederationPlugin } from 'webpack';

export default {
  plugins: [
    new ModuleFederationPlugin({
      name: 'product_app',                              // 应用唯一标识
      filename: 'remoteEntry.js',                        // 入口文件名
      exposes: {
        './ProductList': './src/components/ProductList.vue',
        './ProductDetail': './src/components/ProductDetail.vue',
        './useProducts': './src/composables/useProducts.ts',
      },
      shared: {
        vue: {
          singleton: true,                               // 全局单例
          requiredVersion: '^3.4.0',                     // 版本约束
          eager: false,                                  // 懒加载
        },
        pinia: { singleton: true, requiredVersion: '^2.1.0' },
        'element-plus': { singleton: true },
      },
    }),
  ],
};

React 子应用(组件暴露):

// webpack.config.ts(React 子应用)
new ModuleFederationPlugin({
  name: 'user_app',
  filename: 'remoteEntry.js',
  exposes: {
    './UserProfile': './src/components/UserProfile.tsx',
    './UserList': './src/pages/UserList.tsx',
    './useAuth': './src/hooks/useAuth.ts',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^18.2.0' },
    'react-dom': { singleton: true },
    zustand: { singleton: true },
    antd: { singleton: true },
  },
}),

3.2.2 主应用(Host)—— 消费远程模块

// webpack.config.ts(主应用)
new ModuleFederationPlugin({
  name: 'main_app',
  remotes: {
    // key => 远程应用名,value => 远程入口 URL
    product_app: 'product_app@http://localhost:3001/remoteEntry.js',
    user_app: 'user_app@http://localhost:3002/remoteEntry.js',
  },
  shared: {
    vue: { singleton: true, requiredVersion: '^3.4.0' },
    react: { singleton: true, requiredVersion: '^18.2.0' },
    'react-dom': { singleton: true },
    pinia: { singleton: true },
    zustand: { singleton: true },
  },
}),

3.2.3 消费远程模块

在 Vue 3 主应用中加载 Vue 远程组件:

<!-- MainApp.vue -->
<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';

// 方式一:静态导入远程组件
const RemoteProductList = defineAsyncComponent(
  () => import('product_app/ProductList')
);

// 方式二:动态按需加载
const remoteComponent = ref<Component | null>(null);

async function loadRemoteModule() {
  try {
    const module = await import('user_app/UserProfile');
    remoteComponent.value = module.default;
  } catch (error) {
    console.error('远程模块加载失败:', error);
    // 防御式编程:永远不要信任远程模块可用
    remoteComponent.value = FallbackComponent;
  }
}
</script>

<template>
  <div>
    <h3>远程模块示例</h3>
    <Suspense>
      <RemoteProductList />
      <template #fallback>
        <el-skeleton :rows="3" animated />
      </template>
    </Suspense>
  </div>
</template>

在 React 主应用中加载 React 远程组件:

// App.tsx
import React, { Suspense, lazy, ComponentType } from 'react';
import { Spin } from 'antd';

// 方式一:静态 lazy 加载(推荐)
const RemoteUserList = lazy(() => import('user_app/UserList'));

// 方式二:动态加载 + 错误边界
function useRemoteModule<T>(loader: () => Promise<{ default: T }>) {
  const [module, setModule] = React.useState<T | null>(null);
  const [error, setError] = React.useState<Error | null>(null);

  React.useEffect(() => {
    loader()
      .then((mod) => setModule(mod.default))
      .catch((err) => {
        console.error('远程模块加载失败:', err);
        setError(err);
      });
  }, []);

  return { module, error };
}

function App() {
  const { module: UserProfile, error } = useRemoteModule(
    () => import('user_app/UserProfile')
  );

  return (
    <div>
      <Suspense fallback={<Spin tip="加载远程模块..." />}>
        <RemoteUserList />
      </Suspense>

      {error && <Alert message="远程服务不可用" type="warning" />}
    </div>
  );
}

3.3 Vite Module Federation

Vite 原生不支持 Module Federation,需要通过社区插件 @originjs/vite-plugin-federation

// vite.config.ts(远程应用)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'product_app',
      filename: 'remoteEntry.js',
      exposes: {
        './ProductCard': './src/components/ProductCard.vue',
        './store': './src/stores/productStore.ts',
      },
      shared: ['vue', 'pinia'],
    }),
  ],
  build: {
    target: 'esnext',    // 必须设置为 esnext
    minify: false,
    cssCodeSplit: false,
  },
});
// vite.config.ts(主应用)
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    vue(),
    federation({
      name: 'main_app',
      remotes: {
        product_app: 'http://localhost:5001/assets/remoteEntry.js',
      },
      shared: ['vue', 'pinia'],
    }),
  ],
  build: {
    target: 'esnext',
    minify: false,
    cssCodeSplit: false,
  },
});

Vite Module Federation 注意事项:

注意点 说明
build.target 必须为 esnext 否则 remoteEntry 会报语法错误
minify 建议关闭 产物体积稍大,但避免远程模块加载异常
cssCodeSplit 关闭 否则子应用样式不会随 remoteEntry 一起加载
dev 模式不支持 Vite 插件仅在生产构建时生效,开发阶段需通过其他方式联合调试
版本兼容 @originjs/vite-plugin-federation 版本需与 Vite 版本匹配

3.4 共享依赖(shared)策略

shared 配置是 Module Federation 最核心也最易出错的配置。

shared: {
  // ✅ 正确:指定版本范围,使用单例
  vue: {
    singleton: true,
    requiredVersion: '^3.4.0',
    eager: false,
  },

  // ❌ 错误:不指定版本,可能导致多个 Vue 实例
  vue: {},

  // ❌ 错误:singleton: false,导致重复加载
  vue: { singleton: false },

  // ✅ 场景2:需要立即加载的共享库
  'core-js': { eager: true },

  // ✅ 场景3:第三方 UI 组件库
  'element-plus': { singleton: true, requiredVersion: '^2.8.0' },
  antd: { singleton: true, requiredVersion: '^5.12.0' },
}

shared 配置选项详解:

选项 说明 推荐值 踩坑说明
singleton 是否全局单例 true (UI库/框架) false 会导致多个 Vue/React 实例,引发异常
requiredVersion 版本约束 ^x.y.z 宽松范围 过严导致加载失败,过松导致版本不兼容
eager 是否立即加载(非异步 Chunk) 工具库 true,UI 库 false true 增大首屏体积
import 指定 fallback 模块 一般不设置 设置后会导致本地再打包一份

shared 配置原则:

┌─────────────────────────────────────────┐
│          shared 配置决策过程              │
├─────────────────────────────────────────┤
│                                         │
│  UI 框架(Vue/React) → singleton: true  │
│  UI 组件库(Antd/Element) → singleton   │
│  状态管理(Pinia/Zustand) → singleton    │
│                                         │
│  工具库(lodash/dayjs) → 不共享或自愿    │
│  业务组件 → 不共享(通过 exposes 暴露)    │
│                                         │
└─────────────────────────────────────────┘

3.5 跨技术栈:Vue 应用加载 React 组件

Module Federation 天然支持跨框架加载,但需要约定组件接口。

React 远程组件(暴露统一 Props 接口):

// user_app/src/components/UserProfile.tsx
import React from 'react';
import { Card, Descriptions } from 'antd';

// 统一 Props 接口(Vue 侧通过 attrs 传递)
export interface UserProfileProps {
  userId: string;
  onLogout?: () => void;
}

const UserProfile: React.FC<UserProfileProps> = ({ userId, onLogout }) => {
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then((res) => res.json())
      .then(setUser)
      .catch(() => setUser(null)); // 防御式编程
  }, [userId]);

  return (
    <Card>
      <Descriptions title="用户信息">
        <Descriptions.Item label="ID">{user?.id ?? '---'}</Descriptions.Item>
        <Descriptions.Item label="姓名">{user?.name ?? '---'}</Descriptions.Item>
      </Descriptions>
      {onLogout && <button onClick={onLogout}>退出登录</button>}
    </Card>
  );
};

export default UserProfile;

Vue 主应用中加载:

<script setup lang="ts">
import { defineAsyncComponent, ref } from 'vue';

// Vue 加载 React 组件需要包裹层
const ReactUserProfile = defineAsyncComponent(
  () => import('user_app/UserProfile')
);

const currentUserId = ref('12345');

function handleLogout() {
  console.log('退出登录');
}
</script>

<template>
  <div class="vue-container">
    <h3>Vue 主应用加载 React 组件</h3>
    <Suspense>
      <!-- Vue 模板中直接使用 React 组件,Props 通过 kebab-case 传递 -->
      <ReactUserProfile
        :user-id="currentUserId"
        :on-logout="handleLogout"
      />
      <template #fallback>
        <el-skeleton :rows="4" animated />
      </template>
    </Suspense>
  </div>
</template>

跨框架通信约定:

约定 说明 示例
Props 接口 使用纯 JavaScript 类型(string / number / function) userId: string, onLogout: () => void
避免传复杂对象 跨框架传递 Proxy 对象可能引发问题 string ID 而非 User 对象
避免传 reactive 对象 Vue 的 ref/reactive 对 React 不可用 使用普通值或函数回调
事件回调 通过 Props 传递函数 : 绑定即可
样式隔离 使用 CSS Modules 或 CSS-in-JS 避免全局样式污染

四、Qiankun 实践

4.1 原理概述

Qiankun 基于 single-spa,在路由变化时加载/卸载子应用,核心机制:

  1. HTML Entry:主应用通过 fetch 加载子应用的 HTML,解析后提取 JS/CSS 执行
  2. Proxy 沙箱:通过 Proxy 劫持 window 对象,隔离子应用的全局变量
  3. 样式隔离:通过 Shadow DOM 或 Scoped CSS 隔离子应用样式
  4. 生命周期:子应用导出 bootstrap / mount / unmount 三阶段
用户访问 /product 
    │
    ▼
主应用检测到路由变化,激活 product_app
    │
    ├── fetch http://product-app/  →  加载 HTML Entry
    ├── 解析 HTML,提取 CSS / JS
    ├── 创建 Proxy 沙箱
    ├── 执行子应用 JS(在沙箱中)
    ├── 调用子应用 mount 生命周期
    └── 渲染完成

4.2 主应用(基座)配置

// main-app/src/main.ts(Vue 3 主应用)
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
import {
  registerMicroApps,
  start,
  initGlobalState,
  MicroAppStateActions,
} from 'qiankun';

const app = createApp(App);
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('./views/Home.vue') },
    // 子应用路由不需要在主应用中定义,由子应用自身管理
  ],
});

app.use(router);
app.use(createPinia());
app.use(ElementPlus);
app.mount('#main-app');

// ==================== Qiankun 配置 ====================

// 1. 注册子应用
registerMicroApps([
  {
    name: 'product-app',                                 // 子应用名称(唯一)
    entry: '//localhost:3001',                            // 子应用入口 HTML
    container: '#sub-app-container',                      // 挂载容器
    activeRule: '/product',                               // 激活路由规则
    props: {
      // 传递给子应用的自定义数据
      globalToken: localStorage.getItem('token'),
    },
  },
  {
    name: 'order-app',
    entry: '//localhost:3002',
    container: '#sub-app-container',
    activeRule: '/order',
    props: {},
  },
  {
    name: 'user-app',
    entry: '//localhost:3003',
    container: '#sub-app-container',
    activeRule: '/user',
    props: {},
  },
]);

// 2. 全局状态(应用间通信)
const actions: MicroAppStateActions = initGlobalState({
  user: null,
  token: localStorage.getItem('token'),
});

// 监听状态变化
actions.onGlobalStateChange((state, prev) => {
  console.log('[主应用] 全局状态变化:', state, '->', prev);
});

// 主应用可主动更新状态
export function updateToken(token: string) {
  actions.setGlobalState({ token });
}

// 3. 启动 Qiankun
start({
  sandbox: {
    experimentalStyleIsolation: true,   // 开启样式隔离
  },
  prefetch: 'all',                      // 预加载所有子应用
});

主应用 App.vue 模板:

<!-- App.vue -->
<template>
  <el-container>
    <!-- 主应用导航 -->
    <el-header>
      <el-menu mode="horizontal" router>
        <el-menu-item index="/">首页</el-menu-item>
        <el-menu-item index="/product">商品管理</el-menu-item>
        <el-menu-item index="/order">订单管理</el-menu-item>
        <el-menu-item index="/user">用户管理</el-menu-item>
      </el-menu>
    </el-header>

    <el-main>
      <!-- 主应用自身路由 -->
      <router-view />

      <!-- 子应用挂载容器 -->
      <div id="sub-app-container"></div>
    </el-main>
  </el-container>
</template>

4.3 子应用注册

Vue 3 子应用

// sub-app/src/main.ts(Vue 3 子应用)
import { createApp, App as VueApp } from 'vue';
import { createRouter, createWebHistory, Router } from 'vue-router';
import { createPinia } from 'pinia';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
import routes from './router';
import type { QiankunProps } from './types';

let app: VueApp | null = null;
let router: Router | null = null;

// 子应用的生命周期导出(挂载到 window 上)
export async function bootstrap() {
  console.log('[子应用] product-app bootstrapped');
}

export async function mount(props: QiankunProps) {
  console.log('[子应用] product-app mounted, props:', props);

  const { container, globalState } = props;

  // 创建独立的 Vue 实例
  app = createApp(App);
  router = createRouter({
    // 关键:使用 window.__POWERED_BY_QIANKUN__ 判断,添加路由前缀
    history: createWebHistory(
      window.__POWERED_BY_QIANKUN__ ? '/product' : '/'
    ),
    routes,
  });

  app.use(router);
  app.use(createPinia());
  app.use(ElementPlus);

  // 注入 Qiankun 传递的全局状态
  if (globalState) {
    const store = useGlobalStore();
    store.setToken(globalState.token);
  }

  // 挂载到子应用容器(注意:使用 container 下的选择器)
  app.mount(
    container
      ? container.querySelector('#sub-app')
      : document.getElementById('sub-app')
  );
}

export async function unmount(props: QiankunProps) {
  console.log('[子应用] product-app unmounted');

  app?.unmount();
  app = null;
  router = null;
}
// sub-app/vue.config.ts(Qiankun 子应用 webpack 配置)
const { defineConfig } = require('@vue/cli-service');

module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    port: 3001,
    headers: {
      'Access-Control-Allow-Origin': '*',    // 允许跨域加载
    },
  },
  configureWebpack: {
    output: {
      library: 'product-app',
      libraryTarget: 'umd',                   // 必须 UMD 格式
      chunkLoadingGlobal: 'webpackJsonp_product_app', // 避免 jsonp 冲突
    },
  },
});

React 子应用

// sub-app/src/public-path.ts(必须第一行引入)
if ((window as any).__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = (window as any).__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
// sub-app/src/index.ts(React 子应用入口)
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './public-path';

let root: ReactDOM.Root | null = null;

export async function bootstrap() {
  console.log('[React 子应用] bootstrapped');
}

export async function mount(props: any) {
  const { container, globalState } = props;

  root = ReactDOM.createRoot(
    container
      ? container.querySelector('#root')
      : document.getElementById('root')
  );

  root.render(
    <BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/order' : '/'}>
      <App globalState={globalState} />
    </BrowserRouter>
  );
}

export async function unmount(props: any) {
  root?.unmount();
  root = null;
}

4.4 样式隔离

Qiankun 提供两种样式隔离方案:

方案 配置 原理 优缺点
StrictStyleIsolation sandbox.strictStyleIsolation: true Shadow DOM 包裹 完全隔离,但部分 UI 库弹窗挂载在 body 下导致样式丢失
ExperimentalStyleIsolation sandbox.experimentalStyleIsolation: true 添加属性选择器(div[data-qiankun-app] 兼容性好,但 CSS 选择器权重改变可能覆盖异常
// ✅ 推荐:使用 ExperimentalStyleIsolation
start({
  sandbox: {
    experimentalStyleIsolation: true,
  },
});

// ❌ 不推荐:StrictStyleIsolation 会导致弹窗/Select Dropdown 样式异常

样式隔离踩坑点:

<!-- ❌ 问题:子应用中的弹窗挂载在 body 下,Shadow DOM 无法控制 -->
<template>
  <el-dialog v-model="visible" title="提示">
    <!-- 弹窗 DOM 被追加到 document.body,不在 Shadow DOM 内 -->
    <!-- 子应用样式无法应用到弹窗上 -->
  </el-dialog>
</template>

<!-- ✅ 解决:使用 append-to-body="false" 将弹窗挂载到当前容器 -->
<template>
  <el-dialog
    v-model="visible"
    title="提示"
    :append-to-body="false"
  >
  </el-dialog>
</template>
// React 同理:Ant Design Modal 挂载点问题
<Modal
  open={visible}
  getContainer={() => document.getElementById('sub-app-root')!}
  // ✅ 关键:指定挂载容器,避免跑到 Shadow DOM 外
>
  <p>弹窗内容</p>
</Modal>

4.5 JS 沙箱

Qiankun 的沙箱基于 Proxy 实现,有三种沙箱类型:

沙箱类型 配置 支持多实例 性能 特性
LegacySandbox 单例模式 单应用场景
ProxySandbox 默认 多应用共存场景(推荐)
SnapshotSandbox 不兼容 Proxy 时降级 兼容 IE11
start({
  sandbox: {
    // 默认使用 ProxySandbox
    // 以下配置为严格模式下的选项
    strictStyleIsolation: false,
    experimentalStyleIsolation: true,
  },
});

沙箱原理简析:

// 伪代码:Proxy 沙箱简化版
class ProxySandbox {
  private proxyWindow: Window;

  constructor() {
    const fakeWindow = Object.create(null);

    this.proxyWindow = new Proxy(fakeWindow, {
      get(target, key) {
        // 优先返回 fakeWindow 上的属性
        if (Reflect.has(target, key)) {
          return Reflect.get(target, key);
        }
        // 回退到真实 window
        return (window as any)[key];
      },
      set(target, key, value) {
        // 子应用的全局变量写入 fakeWindow,不污染真实 window
        return Reflect.set(target, key, value);
      },
    });
  }
}

沙箱相关问题排查:

// ❌ 问题1:子应用使用 window.xxx 存储全局变量,卸载后未清理
// ✅ 解决:卸载时在 unmount 中清理
export async function unmount() {
  // 清理定时器
  clearInterval(timer);
  // 清理全局事件
  window.removeEventListener('resize', handleResize);
  // 清理全局变量(Qiankun 沙箱会自动清理,但最好手动)
  delete (window as any).__MY_GLOBAL__;
}

// ❌ 问题2:子应用使用了非标准 Window 属性
const el = window as any;
el.CustomGlobalVar = 'xxx';  // 可能被 Qiankun 沙箱拦截

// ✅ 解决:使用提供的 props 或状态管理
const props = useQiankunProps();
props.setGlobalState({ key: 'xxx' });

4.6 应用间通信

方式一:initGlobalState(官方方案)

// 主应用
import { initGlobalState } from 'qiankun';

const actions = initGlobalState({ user: null, token: '' });

// 主应用更新
actions.setGlobalState({ user: { id: 1, name: '张三' } });

// 子应用获取
export async function mount(props: any) {
  // props 包含 onGlobalStateChange 和 setGlobalState
  props.onGlobalStateChange((state: any, prev: any) => {
    console.log('全局状态变化:', state);
    // 同步到子应用的 Store
    useUserStore().setUser(state.user);
  }, true); // true:立即触发一次

  // 子应用也可以修改全局状态
  props.setGlobalState({ token: 'new-token' });
}

方式二:shared 模块(推荐,更灵活)

在 Module Federation 中使用 shared 暴露通信模块:

// shared-lib/src/communication.ts(独立的公共仓库)
import { reactive } from 'vue';

// 全局状态中心
export const globalStore = reactive<{
  user: { id: string; name: string } | null;
  token: string;
  events: Map<string, Function[]>;
}>({
  user: null,
  token: '',
  events: new Map(),
});

// 发布订阅通信
export function on(event: string, callback: Function) {
  if (!globalStore.events.has(event)) {
    globalStore.events.set(event, []);
  }
  globalStore.events.get(event)!.push(callback);

  // 返回取消订阅函数
  return () => {
    const cbs = globalStore.events.get(event);
    if (cbs) {
      const idx = cbs.indexOf(callback);
      if (idx > -1) cbs.splice(idx, 1);
    }
  };
}

export function emit(event: string, ...args: any[]) {
  globalStore.events.get(event)?.forEach((cb) => cb(...args));
}

跨技术栈通信方案对比:

方式 适用场景 复杂度 风险
initGlobalState Qiankun 体系内 状态多时维护困难
shared 模块 Module Federation 需保证 singleton
CustomEvent 任意方案 事件名冲突,无类型安全
URL 参数 简单数据传递 最低 只能传字符串
后端统一状态 登录态等全局数据 增加网络开销

五、微前端实战要点

5.1 公共依赖提取

方案对比:

策略 说明 优点 缺点
CDN 外链 框架/Vue/React 走 CDN,不打包 减小体积 版本锁定,升级不便
shared(Module Federation) 运行时共享依赖 按需加载,版本协商 配置复杂
External 配置 webpack externals 排除框架 体积最小 需要手动管理 CDN
公共 NPM 包 公共逻辑抽为独立包,子应用引用 类型安全 版本升级需要所有子应用重新构建

推荐做法:

// 方案:Module Federation shared + external 混合

// 子应用 webpack 配置
module.exports = {
  externals: {
    // 将大型 UI 库设为 external,避免重复打包
    'element-plus': 'ElementPlus',
    vue: 'Vue',
  },
};

公共依赖提取原则:

依赖类型 建议 原因
Vue / React 框架 Webpack external + CDN 体积大(>100KB),版本统一
UI 组件库 shared singleton 组件多,提取后大幅减少体积
工具库(lodash/dayjs) 子应用自包含 体积小,版本差异影响小
业务公共组件 remote exposes 统一管理,避免重复实现
状态管理库 shared singleton 保证 Store 是同一实例

5.2 路由分发

核心问题: 主应用路由与子应用路由如何协同工作?

方案:主应用控制路由前缀,子应用管理内部路由

URL 设计:
/
├── /product                  → 激活 Product 子应用
│   ├── /product/list         → 商品列表(子应用内部路由)
│   └── /product/:id          → 商品详情(子应用内部路由)
├── /order                    → 激活 Order 子应用
│   ├── /order/list           → 订单列表(子应用内部路由)
│   └── /order/:id            → 订单详情(子应用内部路由)
└── /user                     → 激活 User 子应用
    └── /user/profile         → 用户信息(子应用内部路由)

Qiankun 路由创建:

// 子应用:根据是否在 Qiankun 中添加 basename
// Vue Router
const router = createRouter({
  history: createWebHistory(
    window.__POWERED_BY_QIANKUN__ ? '/product' : '/'
  ),
  routes,
});

// React Router
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/order' : '/'}>
  <App />
</BrowserRouter>

路由分发注意事项:

// ✅ 正确:主应用不重复定义子应用路由
const routes = [
  { path: '/', component: Home },
  // 不需要定义 /product/** 的路由,Qiankun 通过 activeRule 控制
];

// ❌ 错误:主应用和子应用都定义相同路由,导致重复渲染
const routes = [
  { path: '/product', component: ProductApp },  // 错误!
];

// ✅ 正确:主应用只提供一个容器,路由匹配由 Qiankun 的 activeRule 决定

5.3 登录态共享

推荐方案:SSO + Token 透传

用户登录
    │
    ▼
主应用获取 Token (JWT)
    │
    ├── Token 存入 localStorage(同一域名下)
    ├── 通过 Qiankun props 或 shared 状态传递到子应用
    ├── 子应用从 props/globalState 获取 Token
    │
    ▼
子应用发请求时携带 Token(Authorization Header)
    │
    ▼
后端验证 Token,返回数据

主应用传递 Token:

// 主应用:登录后更新
const actions = initGlobalState({
  token: localStorage.getItem('token') || '',
  user: null,
});

// Token 续期
function refreshToken(newToken: string) {
  localStorage.setItem('token', newToken);
  actions.setGlobalState({ token: newToken });
}

子应用获取 Token:

// 子应用:从 Qiankun props 获取
export async function mount(props: any) {
  const { token } = props;

  // 方式1:直接设置请求头
  request.defaults.headers.common['Authorization'] = `Bearer ${token}`;

  // 方式2:存入本地状态管理
  const authStore = useAuthStore();
  authStore.setToken(token);
}

// 方式3(推荐):封装请求库中自动提取
import axios from 'axios';

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
});

// 请求拦截器:自动携带 Token
request.interceptors.request.use((config) => {
  // 优先从 Qiankun 全局状态获取
  let token = '';

  if (window.__POWERED_BY_QIANKUN__) {
    // 通过 props 获取(需要从 store 或全局变量读取)
    token = useAuthStore().token;
  } else {
    // 独立运行时从 localStorage 获取
    token = localStorage.getItem('token') || '';
  }

  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }

  return config;
});

5.4 部署与版本管理

独立部署架构:

CDN (OSS / CloudFront)
│
├── /main-app/            ← 主应用(基座)
│   ├── index.html
│   ├── assets/main.xxx.js
│   └── assets/main.xxx.css
│
├── /product-app/         ← 子应用 A(Vue 3)
│   ├── index.html
│   ├── assets/product.xxx.js
│   └── remoteEntry.js    ← Module Federation 入口
│
├── /order-app/           ← 子应用 B(React)
│   ├── index.html
│   ├── assets/order.xxx.js
│   └── remoteEntry.js    ← Module Federation 入口
│
└── /user-app/            ← 子应用 C
    └── ...

版本管理策略:

策略 说明 适用场景
同版本发布 主应用+子应用统一版本 小团队,发布频率一致
独立版本+兼容 子应用各自版本,主应用通过配置引用 大团队,独立迭代
灰度发布 主应用维护多版本 remoteEntry 映射 大型平台,需要灰度能力

版本管理示例(主应用配置化):

// main-app/src/config/apps.ts
// 主应用通过配置文件管理子应用版本,实现灰度发布
interface AppConfig {
  name: string;
  entry: string;
  activeRule: string;
  version: string;
  grayUsers?: string[];   // 灰度用户白名单
}

const appConfigs: Record<string, AppConfig> = {
  'product-app': {
    name: 'product-app',
    entry: '//cdn.example.com/product-app/2.1.0/',     // 通过版本号控制
    activeRule: '/product',
    version: '2.1.0',
    grayUsers: ['user_001'],                          // 灰度用户走 2.2.0-beta
  },
  'order-app': {
    name: 'order-app',
    entry: '//cdn.example.com/order-app/1.3.0/',
    activeRule: '/order',
    version: '1.3.0',
  },
};

5.5 CI/CD 独立部署流程

子应用独立部署流程:

┌──────────┐    ┌───────────┐    ┌──────────┐    ┌───────────┐
│  开发者   │    │  CI/CD    │    │    CDN   │    │  主应用   │
│  push代码  │    │  构建     │    │  上传    │    │  加载     │
└────┬─────┘    └─────┬─────┘    └────┬─────┘    └─────┬─────┘
     │                │               │                │
     │── git push ───→│               │                │
     │                │── npm run build → │            │
     │                │               │── upload /dist │
     │                │               │──→ CDN 资源    │
     │                │               │                │
     │                │── 通知主应用    │                │
     │                │  版本已更新   │                │
     │                │───────────────│────────────────│
     │                │               │                │── 用户访问
     │                │               │── 加载新版     │← 加载新资源
     │                │               │   remoteEntry  │

CI/CD 配置示例(GitHub Actions):

# .github/workflows/product-app-deploy.yml
name: Deploy Product App

on:
  push:
    branches: [main]
    paths:                  # 只有 product-app 目录变化才触发
      - 'apps/product-app/**'

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install pnpm
        run: npm install -g pnpm

      - name: Install dependencies
        run: pnpm install

      - name: Build product app
        run: pnpm --filter product-app build

      - name: Upload to CDN
        uses: jakejarvis/s3-sync-action@master
        with:
          args: --acl public-read --follow-symlinks --delete
        env:
          AWS_S3_BUCKET: ${{ secrets.CDN_BUCKET }}
          SOURCE_DIR: 'apps/product-app/dist'
          DEST_DIR: 'product-app/${{ github.sha }}'   # 版本号

      - name: Update version config
        run: |
          # 更新主应用的版本配置(通过 API 或更新配置文件)
          curl -X POST https://api.example.com/config/update \
            -H "Authorization: Bearer ${{ secrets.DEPLOY_TOKEN }}" \
            -d '{"app":"product-app","version":"${{ github.sha }}"}'

六、微前端 vs Monorepo 选型

6.1 对比维度

维度 微前端 Monorepo
仓库数量 多个独立仓库(Multi-repo) 单一仓库管理多个项目
构建方式 独立构建,独立部署 统一构建,整体部署
发布粒度 子应用单独发布 一次构建发布所有包
技术栈 任意技术栈混用 技术栈必须统一
代码共享 通过模块联邦/NPM 包 通过 workspace 直接引用
依赖管理 各自管理,可能重复 统一管理,依赖提升
团队协作 完全隔离,代码不共享 代码互访,便于重构
构建时间 小(单应用 < 30s) 大(全量构建 > 10min)
运行时隔离 沙箱隔离,样式/JS 互不干扰 无隔离,同一运行时
学习成本 高(沙箱/通信/部署复杂) 低(常规开发模式)
调试难度 高(跨应用调试困难) 低(IDE 直接调试)

6.2 工程复杂度对比

复杂度曲线:
             ▲
             │                          ╱ 微前端
   工程复杂度   │                    ╱
             │              ╱────────── Monorepo
             │         ╱
             │    ╱────────── 单体前端
             │ ╱
             └─────────────────────────────→
                        团队规模

6.3 选型决策

条件 推荐方案 原因
1~5 人团队,代码 < 20 万行 单体前端 简单直接,复杂度最低
1~10 人团队,技术栈统一,多包复用 Monorepo + pnpm workspace 代码复用好,构建可优化
10+ 人团队,多业务域,独立部署需求 微前端 独立开发、独立部署、独立运维
10+ 人团队,但技术栈统一,高频协作 Monorepo + Module Federation 结合两者优点
存量系统渐进迁移 微前端(Qiankun / Micro-app) 新老共存,逐步替换

6.4 推荐:Monorepo + Module Federation 混合方案

# 实践中的最佳组合

仓库组织:Monorepo(pnpm workspace)
  │
  ├── packages/
  │   ├── shared-lib/            # 公共工具库
  │   ├── ui-components/         # 公共 UI 组件
  │   └── config/                # 公共配置
  │
  ├── apps/
  │   ├── main-app/              # 主应用(基座)
  │   ├── product-app/           # 商品子应用
  │   └── order-app/             # 订单子应用
  │
  构建方式:
  ├── dev 模式 → pnpm --filter 独立启动
  ├── 公共包变更 → pnpm build:changed(只构建受影响的包)
  └── 子应用发布 → 独立构建 + CDN 上传

Monorepo + Module Federation 的好处:

优势 说明
源码共享 公共类型/工具通过 workspace 引用,无需发布 NPM 包
开发体验 本地可同时启动多个子应用,统一调试入口
独立部署 每个子应用可独立构建、独立发布到 CDN
共享受限 运行时通过 Module Federation shared 配置共享框架依赖
渐进迁移 新技术可以从一个子应用开始,不影响其他模块

七、常见问题与踩坑记录

7.1 Module Federation 常见问题

问题 原因 解决
Shared module is not available for eager consumption shared 模块的 eager 配置不一致 确保主应用/远程应用的 shared 配置一致,或关闭 eager
远程模块加载 404 remoteEntry.js 路径配置错误 检查 remotes 中的 URL 是否为完整可访问路径
版本不兼容导致加载失败 shared 的 requiredVersion 过于严格 放宽版本范围,如 ^3.x.x
Vue / React 多实例 shared singleton: false 或版本不一致 确保 singleton: true,且版本范围兼容
CSS 丢失 Vite 构建未关闭 cssCodeSplit cssCodeSplit: false
TypeError: Cannot read properties of null (reading 'call') 远程应用未正确暴露模块 检查 exposes 配置以及构建产物

7.2 Qiankun 常见问题

问题 原因 解决
子应用切换后内存泄漏 定时器/事件监听未在 unmount 清理 在 unmount 中清理所有副作用
样式冲突 主应用和子应用使用相同 CSS 类名 开启 experimentalStyleIsolation,或子应用 CSS 加 Scope
子应用弹窗显示异常 弹窗挂载到 body 导致样式隔离失效 使用 append-to-body / getContainer 指定容器
Webpack JSONP 冲突 多个子应用使用相同 chunkLoadingGlobal 每个子应用设置唯一的 chunkLoadingGlobal
子应用路由不生效 basename 未根据 Qiankun 环境动态设置 通过 window.__POWERED_BY_QIANKUN__ 判断添加 basename
开发环境子应用加载失败 CORS 跨域限制 子应用配置 devServer.headers['Access-Control-Allow-Origin'] = '*'

7.3 通用调试 Checklist

□ 确认主应用已成功启动 Qiankun(start() 被调用)
□ 确认子应用正确导出了 bootstrap/mount/unmount 生命周期
□ 确认子应用的 webpack output.library 和 libraryTarget 配置正确
□ 确认子应用 remoteEntry.js/CDN 地址可正常访问
□ 确认 Shared 依赖的版本范围兼容(singleton: true)
□ 确认子应用的 basename 路由配置正确(__POWERED_BY_QIANKUN__)
□ 确认样式隔离模式下弹窗/Select 下拉框的挂载容器配置
□ 确认子应用无未清理的定时器、事件监听(内存泄漏检查)
□ 确认跨域相关 Header(CORS)已正确配置
□ 确认 micro-app 或 Qiankun 的版本与框架版本兼容

八、总结

8.1 最终选型建议

场景 推荐方案 核心理由
阿里系中台,Vue 技术栈 Qiankun 生态完善,文档齐全,阿里内部大量验证
需要零改造接入存量系统 Micro-app / Wujie WebComponent 方案,子应用几乎无感知
技术栈统一,追求极致性能 Module Federation 零沙箱开销,native 模块加载,SSR 支持
新项目,团队 5~15 人 Monorepo + pnpm + Module Federation 兼顾开发体验和运行时性能
大型平台,多团队,多技术栈 Qiankun + Module Federation(互补) Qiankun 做沙箱隔离,MF 做模块共享

8.2 核心原则

1. 不要为了微前端而微前端 —— 引入微前端意味着增加复杂度
2. 沙箱隔离不是免费的 —— Proxy 沙箱和 Shadow DOM 都有性能开销
3. 子应用间通信越少越好 —— 全局状态应该尽量精简
4. 版本兼容性是最大挑战 —— 建立完善的版本管理和兼容性测试机制
5. 部署自动化是前提 —— 没有独立部署能力就不要谈微前端
6. 优先考虑 Monorepo + Module Federation 的轻量方案

8.3 关联文档