你是不是也遇到过这种情况:项目刚开始时,代码清爽,构建飞快,改个 Bug 几秒钟就能看到效果。但随着业务不断迭代,项目越来越大,构建时间从 30 秒变成 5 分钟,再变成 10 分钟。改一行代码,等半天才能看到效果,开发体验直线下降。

更要命的是,团队越来越大,不同团队用不同的技术栈:老项目用 Vue 2,新项目想用 React,结果只能硬着头皮继续用 Vue 2,技术债越积越多。每次发版都是惊心动魄,生怕一个小改动影响了其他模块,导致线上故障。

这时候,你可能听说过"微前端"这个概念。但市面上方案这么多:qiankun、micro-app、Module Federation……到底该选哪个?每个方案都说自己好,但真正用起来坑在哪里?

这篇文章不讲虚的,只讲实战。我会从真实项目经验出发,深度对比这三种主流微前端方案,告诉你:

  • 它们的核心原理是什么
  • 各自的优缺点在哪里
  • 什么场景适合用哪个方案
  • 如何做出正确的技术选型决策

如果你正在考虑引入微前端,这篇文章能帮你少踩很多坑。

微前端发展历程

微前端从 2016 年发展至今,经历了三个阶段。了解这个历程,能帮你理解为什么会有这么多方案。

graph LR A[2016
single-spa
能不能做] --> B[2019
qiankun
怎么做更简单] B --> C[2021
micro-app
更轻量] C --> D[2022
Module Federation
性能更好] style A fill:#e1f5ff style B fill:#fff4e1 style C fill:#f0f9ff style D fill:#e8f5e9
什么是"运行时集成"和"构建时集成"?

运行时集成(qiankun、micro-app):

  • 用户打开网页后,浏览器再去下载子应用的代码
  • 类比:就像你去餐厅点菜,厨师现做现卖(等待时间长)
  • 特点:灵活,但性能有开销

构建时集成(Module Federation):

  • 打包时就把代码合并好,用户打开网页时直接用
  • 类比:就像你买盒饭,提前做好了,拿到就能吃(快)
  • 特点:性能好,开发体验好

核心趋势:从"运行时集成"向"构建时集成"演进,性能和开发体验越来越好。

1. 为什么需要微前端

1.1 单体应用的三大痛点

先说说为什么会有微前端这个东西。如果单体应用没问题,谁会没事找事拆分架构?

痛点一:构建时间越来越长

真实场景:我之前维护过一个金融交易平台,刚开始只有交易下单、资产查询几个模块,Webpack 构建只要 30 秒。两年后,加了资管、风控、清算、做市等十几个模块,构建时间飙到 12 分钟。

为什么会这样

  • 代码量从 5 万行涨到 50 万行
  • 依赖包从 50 个涨到 200 个
  • Webpack 要处理的文件越来越多

影响

  • 开发时热更新慢,改一行代码等半天
  • CI/CD 流水线变慢,发版时间从 5 分钟变成 20 分钟
  • 开发体验极差,工程师怨声载道
痛点二:部署风险高

真实场景:有一次我们改了一个资管模块的小 Bug,结果发版后交易模块出问题了。原因是两个模块共用了一个工具函数,改动影响了交易模块的逻辑。

为什么会这样

  • 所有代码打包在一起,牵一发动全身
  • 改一个模块,整个应用都要重新构建、测试、发版
  • 回滚成本高,一个模块出问题,整个应用都要回滚

影响

  • 发版变得小心翼翼,不敢频繁迭代
  • 测试成本高,每次都要全量回归测试
  • 线上故障风险大
痛点三:技术栈被锁死

真实场景:我们的较早的项目用的是 Vue 2,团队想用 Vue 3 或 React 重构新模块,但因为是单体应用,要么全部重构(成本太高),要么继续用 Vue 2(技术债越积越多)。

为什么会这样

  • 单体应用只能用一个技术栈
  • 技术升级要么全量升级,要么不升级
  • 无法尝试新技术,团队技术栈老化

影响

  • 招人难,新人不愿意用老技术
  • 技术债越积越多,维护成本越来越高
  • 团队技术能力停滞不前

1.2 微前端如何解决这些问题

微前端的核心思想很简单:把一个大应用拆成多个小应用,每个小应用独立开发、独立部署、独立运行

微前端的三大核心能力

1. 独立开发

  • 每个子应用有自己的代码仓库
  • 不同团队维护不同子应用,互不干扰
  • 构建时间大幅缩短(只构建自己的子应用)

2. 独立部署

  • 改一个子应用,只需要发布这个子应用
  • 其他子应用不受影响,降低发版风险
  • 可以灰度发布、快速回滚

3. 技术栈无关

  • 主应用用 Vue,子应用可以用 React
  • 老模块继续用 Vue 2,新模块用 Vue 3
  • 可以逐步迁移技术栈,不用一次性重构

举个例子

还是那个金融交易平台,拆成微前端后:

  • 主应用:负责导航、权限、布局(React)
  • 交易系统子应用:独立仓库,独立部署(React)
  • 资管系统子应用:独立仓库,独立部署(Vue 3)
  • 开户系统子应用:独立仓库,独立部署(Vue 2)
  • 做市系统子应用:独立仓库,独立部署(React)
  • 风控系统子应用:独立仓库,独立部署(React)

效果

  • 构建时间:从 12 分钟降到 2 分钟(只构建改动的子应用)
  • 发版风险:改资管模块,只发布资管子应用,不影响交易
  • 技术栈:新模块用 Vue 3/React,老模块继续用 Vue 2,逐步迁移

1.2.1 有同事问:为什么不用 Monorepo?

有次在分享会上讲完这些后,有同事问:"既然项目大,为什么不直接用 Monorepo 架构呢?Monorepo 也能把代码拆分成多个包,也能独立开发,为什么还要搞微前端?"

这是个好问题!很多人容易把 Monorepo 和微前端混淆。让我们先分别理解它们是什么,再看它们的区别

一、Monorepo 是什么?
Monorepo 的定义(行业标准)

定义:Monorepo(Monolithic Repository)是一种代码管理策略,把多个项目的代码放在一个 Git 仓库里统一管理。

解决什么问题

  • 多项目依赖管理:A项目依赖B项目,版本同步很麻烦
  • 代码共享困难:公共组件、工具函数要复制粘贴
  • 工具链不统一:每个项目都要配置一遍 ESLint、Prettier

典型工具:Yarn Workspaces、pnpm workspace

推荐做法

  • 统一依赖版本管理(所有项目用同一个 package.json)
  • 共享构建配置(统一的 Webpack、Vite 配置)
  • 增量构建(只构建改动的包,提高效率)

目的:提高开发效率,减少重复配置,方便代码共享。

二、微前端是什么?
微前端的定义(行业标准)

定义:微前端(Micro Frontends)是一种应用架构模式,把一个大型前端应用拆分成多个独立的子应用,每个子应用可以独立开发、独立部署、独立运行。

解决什么问题

  • 大型应用构建慢:代码量大,构建时间长
  • 发版风险高:改一个模块,整个应用都要重新部署
  • 技术栈被锁死:老项目用 Vue 2,新项目想用 React,但改不了
  • 团队协作困难:多个团队维护同一个代码库,容易冲突

典型方案

  • 微前端框架:qiankun(阿里,基于 single-spa)、micro-app(京东,基于 Web Components)
  • 构建工具方案:Module Federation(Webpack 5/Rspack/Vite)
  • 传统方案:iframe(隔离性好但有性能和体验问题)

推荐做法

  • 独立构建:每个子应用独立打包,生成独立的 JS 文件
  • 独立部署:每个子应用部署到不同的地址
  • 运行时集成:主应用在浏览器里动态加载子应用
  • 隔离机制:JS 沙箱、样式隔离,避免子应用互相影响

目的:降低发版风险,支持技术栈多样性,提高团队协作效率。

三、分阶段对比

Monorepo 和微前端不是同一层面的概念,它们在不同阶段发挥作用:

阶段 Monorepo 微前端
开发阶段 统一管理代码,方便共享依赖和工具配置 每个子应用独立开发,互不干扰
构建阶段 增量构建,只构建改动的包 每个子应用独立构建,生成独立的 JS 文件
部署阶段 不影响部署方式(可以是单体部署,也可以是独立部署) 每个子应用独立部署到不同的地址
运行时阶段 不影响运行方式(用户感知不到 Monorepo) 主应用动态加载子应用,按需加载
用户感受 用户无感知(Monorepo 只是开发者的工具) 首屏加载快(按需加载),模块更新不影响其他模块
四、它们的关系
为什么不只用 Monorepo?

核心原因:Monorepo 解决不了独立部署和发版风险的问题。

具体来说

  • Monorepo 只是把代码放在一个仓库里,但不改变部署方式
  • 如果你的应用是单体架构,用了 Monorepo 后,还是要整体部署
  • 改一个模块,整个应用都要重新构建、测试、发版
  • 回滚某个模块,需要回滚整个仓库或手动 cherry-pick

所以:如果你的痛点是"发版风险高、想独立部署",只用 Monorepo 是解决不了的,必须用微前端。

Monorepo + 微前端结合使用是什么样子?

代码仓库结构

finance-platform/          (Monorepo 仓库)
├── apps/
│   ├── shell/             (主应用)
│   ├── trading/           (交易子应用)
│   ├── asset/             (资管子应用)
│   └── risk/              (风控子应用)
├── packages/
│   ├── shared-components/ (共享组件库)
│   └── shared-utils/      (共享工具函数)
└── package.json           (统一依赖管理)

构建流程

  • 每个子应用独立构建:pnpm build --filter=trading
  • 生成独立的产物:trading/dist/index.js
  • 共享的组件和工具函数可以直接引用,不用复制粘贴

部署流程

  • 每个子应用独立部署到不同的地址:
    • https://finance.com/apps/trading/
    • https://finance.com/apps/asset/
    • https://finance.com/apps/risk/
  • 改资管模块 → 只重新构建 asset → 只部署 /apps/asset/

效果

  • 代码统一管理(Monorepo 的优势)
  • 共享组件和工具函数(Monorepo 的优势)
  • 独立构建、独立部署(微前端的优势)
  • 降低发版风险(微前端的优势)
如何决策:你应该问自己“我”想干什么?
  • 只想统一管理代码、共享依赖?→ 用 Monorepo
  • 需要独立部署、降低发版风险?→ 用 微前端
  • 两者都需要?→ Monorepo + 微前端
五、各自的局限
Monorepo 的局限

1. 回滚问题

  • 场景:资管模块上线后发现Bug,想回滚到上个版本
  • 问题:但这时交易模块已经有新提交了,怎么办?
  • 方案1:回滚整个仓库 → 交易模块也被回滚了(影响其他模块)
  • 方案2:手动 cherry-pick → 工作量大,容易出错

2. 仓库膨胀

  • 随着项目增多,仓库越来越大,clone 和 pull 都很慢
  • Git 操作变慢,影响开发体验

3. 权限管理困难

  • 所有人都能看到所有代码,无法做细粒度的权限控制
微前端的局限

1. 复杂度增加

  • 需要处理子应用通信、路由管理、依赖共享等问题
  • 学习成本和维护成本增加

2. 性能开销

  • 加载多个子应用,网络请求增加
  • JS 沙箱、样式隔离有一定的性能开销

3. 调试困难

  • 多个子应用同时运行,调试和排查问题更复杂

1.3 微前端适用场景

微前端不是银弹,不是所有项目都适合。

适合用微前端的场景

1. 大型项目

  • 代码量 > 10 万行
  • 模块 > 10 个
  • 构建时间 > 5 分钟

2. 多团队协作

  • 团队 > 3 个
  • 不同团队维护不同模块
  • 需要独立开发、独立部署

3. 技术栈迁移

  • 老项目用 Vue 2,想迁移到 Vue 3
  • 想尝试新技术(React、Svelte)
  • 不想一次性重构

4. 高频迭代

  • 需要频繁发版
  • 不同模块发版节奏不同
  • 需要灰度发布、快速回滚
不适合用微前端的场景

1. 小型项目

  • 代码量 < 5 万行
  • 模块 < 5 个
  • 单人或小团队维护

原因:微前端有额外的复杂度(子应用通信、路由管理、依赖共享),小项目用微前端是杀鸡用牛刀。

2. 性能要求极高的项目

  • 首屏加载时间要求 < 1 秒
  • 对运行时性能要求极高

原因:微前端会增加一些性能开销(加载多个子应用、JS 沙箱、样式隔离),如果性能要求极高,需要慎重评估。

3. 团队技术能力不足

  • 团队对前端工程化不熟悉
  • 没有人能 hold 住微前端架构

原因:微前端有一定的学习成本和维护成本,如果团队技术能力不足,可能会适得其反。

1.4 微前端架构图

graph TB subgraph Main["主应用 (Shell)"] A1[路由管理] A2[权限控制] A3[全局状态] A4[子应用加载器] end subgraph Sub1["子应用1 - 交易系统"] B1[现货交易] B2[期货交易] B3[订单管理] end subgraph Sub2["子应用2 - 资管系统"] C1[产品列表] C2[申购赎回] C3[持仓查询] end subgraph Sub3["子应用3 - 开户系统"] D1[客户开户] D2[KYC认证] D3[风险测评] end subgraph Sub4["子应用4 - 做市系统"] E1[做市报价] E2[流动性管理] E3[盈亏分析] end subgraph Sub5["子应用5 - 风控系统"] F1[实时监控] F2[风险预警] F3[授信管理] end A4 -->|动态加载| Sub1 A4 -->|动态加载| Sub2 A4 -->|动态加载| Sub3 A4 -->|动态加载| Sub4 A4 -->|动态加载| Sub5 style Main fill:#e1f5ff style Sub1 fill:#fff4e1 style Sub2 fill:#f0f9ff style Sub3 fill:#e8f5e9 style Sub4 fill:#fff3e0 style Sub5 fill:#fef2f2

2. QianKun框架

2.1 qiankun 是什么?

qiankun 简介

定义:qiankun 是阿里开源的微前端框架,基于 single-spa 封装,提供开箱即用的微前端解决方案。

核心特点

  • 技术栈无关:主应用和子应用可以使用不同的技术栈(React、Vue、Angular 等)
  • JS 沙箱:自动隔离子应用的全局变量,避免污染
  • 样式隔离:支持 Shadow DOM 和 scoped CSS,避免样式冲突
  • 开箱即用:无需复杂配置,几行代码即可接入
  • 生态成熟:社区活跃,文档完善,大厂背书

2.2 qiankun 核心概念

1. 主应用(基座应用)

  • 负责注册和加载子应用
  • 管理全局路由和权限
  • 提供全局状态和通信机制

2. 子应用(微应用)

  • 独立开发、独立部署
  • 导出生命周期函数(bootstrap、mount、unmount)
  • 可以是任何技术栈

3. 生命周期

  • bootstrap:子应用初始化(只执行一次)
  • mount:子应用挂载(每次激活时执行)
  • unmount:子应用卸载(每次切换时执行)

4. 工作原理

qiankun 加载子应用的流程如下:

graph LR A[用户访问 /trading] --> B[主应用路由匹配] B --> C[qiankun 触发加载] C --> D[fetch 子应用 HTML] D --> E[解析提取 JS/CSS] E --> F[创建沙箱并执行 JS] F --> G[调用 mount 渲染到容器]

从流程图可以看出,qiankun 是在浏览器运行时动态加载子应用的全部资源,这就是为什么它被称为「运行时集成」方案。

2.3 快速上手

主应用配置

// main.js
import { registerMicroApps, start } from 'qiankun';

// 注册子应用
registerMicroApps([
  {
    name: 'trading-system',           // 子应用名称
    entry: '//localhost:3001',        // 子应用地址
    container: '#subapp-container',   // 子应用挂载的容器
    activeRule: '/trading',           // 激活路由
  },
  {
    name: 'asset-system',
    entry: '//localhost:3002',
    container: '#subapp-container',
    activeRule: '/asset',
  },
]);

// 启动 qiankun
start();

子应用配置(以 React 为例)

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

let root = null;

// 导出生命周期函数
export async function bootstrap() {
  console.log('交易系统初始化');
}

export async function mount(props) {
  console.log('交易系统挂载', props);

  const container = props.container || document.querySelector('#root');

  root = ReactDOM.createRoot(container);
  root.render(<App />);
}

export async function unmount(props) {
  console.log('交易系统卸载');
  root?.unmount();
}

// 独立运行时的逻辑
if (!window.__POWERED_BY_QIANKUN__) {
  mount({});
}

子应用 Webpack 配置

// webpack.config.js
module.exports = {
  output: {
    library: 'tradingSystem',        // 子应用名称
    libraryTarget: 'umd',            // 必须是 umd 格式
    publicPath: '//localhost:3001/', // 子应用地址
  },
  devServer: {
    port: 3001,
    headers: {
      'Access-Control-Allow-Origin': '*', // 允许跨域
    },
  },
};

2.4 实战:金融平台微前端改造

下面演示如何用 qiankun 把交易系统(React)和资管系统(Vue 3)集成到主应用中,实现独立开发部署、共享登录状态。

主应用配置(React)

// src/App.jsx
import { registerMicroApps, start, initGlobalState } from 'qiankun';
import { useEffect } from 'react';

function App() {
  useEffect(() => {
    // 初始化全局状态(用于应用间通信)
    // user 和 token 从登录接口获取
    const actions = initGlobalState({
      user: { id: 1001, name: '张三', role: 'trader' },
      token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
    });

    // 注册子应用
    registerMicroApps([
      {
        name: 'trading-system',
        entry: '//localhost:3001',
        container: '#subapp-container',
        activeRule: '/trading',
        props: { actions }, // 传递全局状态
      },
      {
        name: 'asset-system',
        entry: '//localhost:3002',
        container: '#subapp-container',
        activeRule: '/asset',
        props: { actions },
      },
    ]);

    // 启动 qiankun
    start({
      sandbox: { strictStyleIsolation: true }, // 开启严格样式隔离
    });
  }, []);

  return (
    <div className="main-app">
      <nav>
        <a href="/trading">交易系统</a>
        <a href="/asset">资管系统</a>
      </nav>
      <div id="subapp-container"></div>
    </div>
  );
}

export default App;

子应用改造(交易系统 - React)

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

let root = null;

export async function bootstrap() {
  console.log('[交易系统] 初始化');
}

export async function mount(props) {
  console.log('[交易系统] 挂载', props);

  // 接收主应用传递的全局状态
  props.actions.onGlobalStateChange((state, prev) => {
    console.log('[交易系统] 全局状态变化', state, prev);
  });

  // 获取用户信息
  const { user, token } = props.actions.getGlobalState();
  console.log('[交易系统] 用户信息', user, token);

  // 渲染应用
  const container = props.container || document.querySelector('#root');

  root = ReactDOM.createRoot(container);
  root.render(<App user={user} token={token} />);
}

export async function unmount(props) {
  console.log('[交易系统] 卸载');
  root?.unmount();
}

// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  mount({
    actions: {
      onGlobalStateChange: () => {},
      getGlobalState: () => ({ user: null, token: null }),
    },
  });
}

子应用改造(资管系统 - Vue 3)

// src/main.js
import { createApp } from 'vue';
import App from './App.vue';

let app = null;

export async function bootstrap() {
  console.log('[资管系统] 初始化');
}

export async function mount(props) {
  console.log('[资管系统] 挂载', props);

  // 接收主应用传递的全局状态
  props.actions.onGlobalStateChange((state, prev) => {
    console.log('[资管系统] 全局状态变化', state, prev);
  });

  // 获取用户信息
  const { user, token } = props.actions.getGlobalState();

  // 渲染应用
  const container = props.container || document.querySelector('#app');

  app = createApp(App);
  app.provide('user', user);
  app.provide('token', token);
  app.mount(container);
}

export async function unmount(props) {
  console.log('[资管系统] 卸载');
  app?.unmount();
}

// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
  mount({
    actions: {
      onGlobalStateChange: () => {},
      getGlobalState: () => ({ user: null, token: null }),
    },
  });
}

运行效果

当用户访问 /trading 路由时,最终的 HTML 结构如下:

<div class="main-app">
  <nav>
    <a href="/trading">交易系统</a>
    <a href="/asset">资管系统</a>
  </nav>
  <div id="subapp-container">
    <!-- qiankun 会把交易系统直接挂载到这个容器中 -->
    <!-- 交易系统的 React 组件渲染内容 -->
  </div>
</div>

从上面的代码可以看出,qiankun 的接入流程很清晰:主应用通过 registerMicroApps 注册子应用,子应用导出 bootstrap/mount/unmount 三个生命周期函数,通过 initGlobalState 实现应用间通信。那 qiankun 用起来到底怎么样?我们来看看它的优缺点。

2.5 qiankun 的优缺点

qiankun 的优点

1. 开箱即用

  • 无需复杂配置,几行代码即可接入
  • 自动处理 JS 沙箱和样式隔离
  • 提供完整的生命周期管理

2. 技术栈无关

  • 主应用和子应用可以使用不同的技术栈
  • 支持 React、Vue、Angular、jQuery 等
  • 老项目可以平滑迁移

3. 生态成熟

  • 阿里大厂背书,社区活跃
  • 文档完善,示例丰富
  • 有大量实战案例可参考

4. 功能完善

  • 支持预加载、预获取
  • 支持应用间通信(全局状态)
  • 支持多种沙箱模式
qiankun 的局限

1. 在浏览器里加载子应用,性能有开销

  • qiankun 是在用户打开网页后,浏览器再去下载子应用的代码,不是在打包时就把代码合并好
  • 类比:就像你点外卖,qiankun 是"现做现送"(用户等待时间长),而构建时集成是"提前做好放保温箱"(用户拿到就能吃)
  • JS 沙箱和样式隔离需要额外的运行时代码,有性能开销
  • 子应用切换时需要重新挂载和卸载,切换有延迟(通常 500ms-1s)
  • 首次加载子应用时,需要下载 JS、解析、执行,用户会感知到加载过程

2. 需要改造项目,学习成本高

  • 核心问题:现有项目不能直接接入,必须改造代码
  • 需要导出生命周期函数(bootstrap、mount、unmount)
  • 需要修改 Webpack 配置(library、libraryTarget、publicPath)
  • 需要理解 qiankun 的沙箱机制和生命周期
  • 团队需要时间学习和适应

3. 没有类型安全,开发体验一般

  • 核心问题:子应用是运行时加载的,TypeScript 无法提供类型检查
  • 主应用调用子应用的方法时,没有类型提示和自动补全
  • IDE 无法跳转到子应用的代码
  • 重构时容易遗漏,只能在运行时发现问题
qiankun 适用场景

适合用 qiankun 的场景

  • 大型项目,需要拆分成多个子应用
  • 多个团队协作,技术栈不统一
  • 老项目需要平滑迁移到新技术栈
  • 需要独立部署和发版
  • 团队有一定的微前端经验

不适合用 qiankun 的场景

  • 小型项目,代码量不大
  • 团队技术能力不足,无法 hold 住微前端
  • 对性能要求极高的场景
  • 需要极强的应用间通信(qiankun 的通信机制相对简单)

三、micro-app:能不能更简单一点?

用了一段时间 qiankun 之后,你可能会有这样的感受:

  • 子应用必须导出三个生命周期函数,改造成本不低
  • Webpack 配置要改成 UMD 格式,新手容易踩坑
  • 主应用注册子应用的代码有点啰嗦

这时候你可能会想:有没有更简单的方案?

京东的 micro-app 就是冲着这个痛点来的。它的核心理念是:像使用 iframe 一样简单,但没有 iframe 的各种问题

3.1 micro-app 的核心思路

micro-app 基于 Web Components 的思想,把子应用封装成一个自定义 HTML 标签:

<!-- 就像用 iframe 一样,但不是 iframe -->
<micro-app name="trading" url="http://localhost:3001"></micro-app>

对比一下 qiankun 的写法:

// qiankun 需要用 JS 注册子应用
registerMicroApps([
  {
    name: 'trading',
    entry: '//localhost:3001',
    container: '#container',
    activeRule: '/trading',
  },
]);
start();

哪个更直观?一目了然。

3.2 快速上手

安装

npm install @micro-zoe/micro-app

主应用配置(React)

// src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import microApp from '@micro-zoe/micro-app';
import App from './App';

// 初始化 micro-app
microApp.start();

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

就这么简单,主应用只需要调用 microApp.start() 初始化一下。

主应用使用子应用

// src/App.jsx
import { useEffect } from 'react';

function App() {
  return (
    <div className="main-app">
      <nav>
        <a href="/trading">交易系统</a>
        <a href="/asset">资管系统</a>
      </nav>

      {/* 直接用标签嵌入子应用,就像用 img 标签一样自然 */}
      <micro-app
        name="trading"
        url="http://localhost:3001"
        baseroute="/trading"
      ></micro-app>
    </div>
  );
}

export default App;
关键属性说明
  • name:子应用的唯一标识,不能重复
  • url:子应用的访问地址
  • baseroute:子应用的基础路由,用于路由匹配

子应用改造

这是 micro-app 最爽的地方:子应用几乎不用改代码

只需要做两件事:

1. 设置跨域(开发环境)

// vite.config.js(子应用)
export default defineConfig({
  server: {
    port: 3001,
    cors: true,  // 允许跨域
    origin: 'http://localhost:3001',  // 保证静态资源路径正确
  },
});

2. 处理路由基础路径(可选)

// src/router.js(子应用)
import { createRouter, createWebHistory } from 'vue-router';

const router = createRouter({
  // 如果是被 micro-app 加载,使用主应用传递的 baseroute
  history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || '/'),
  routes: [...],
});

没了。真的没了。

对比一下 qiankun 子应用要做的事:

qiankun 子应用改造清单
  • 导出 bootstrap、mount、unmount 三个生命周期函数
  • 修改 Webpack 配置(library、libraryTarget、publicPath)
  • 处理独立运行和被加载两种模式
  • 配置跨域

micro-app 把这些复杂度都藏在了框架内部,子应用基本上可以当普通项目来开发。

3.3 实战:金融平台微前端改造

还是用交易系统(React)和资管系统(Vue 3)的例子,看看用 micro-app 怎么做。

主应用配置(React)

// src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import microApp from '@micro-zoe/micro-app';
import App from './App';

microApp.start({
  // 全局配置
  'disable-sandbox': false,  // 开启沙箱(默认开启)
  'disable-scopecss': false, // 开启样式隔离(默认开启)
});

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

主应用路由和子应用嵌入

// src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

// 交易系统页面
function TradingPage() {
  return (
    <micro-app
      name="trading"
      url="http://localhost:3001"
      baseroute="/trading"
      // 向子应用传递数据
      data={{ user: { id: 1001, name: '张三' }, token: 'xxx' }}
    ></micro-app>
  );
}

// 资管系统页面
function AssetPage() {
  return (
    <micro-app
      name="asset"
      url="http://localhost:3002"
      baseroute="/asset"
      data={{ user: { id: 1001, name: '张三' }, token: 'xxx' }}
    ></micro-app>
  );
}

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/trading">交易系统</Link>
        <Link to="/asset">资管系统</Link>
      </nav>

      <Routes>
        <Route path="/trading/*" element={<TradingPage />} />
        <Route path="/asset/*" element={<AssetPage />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

子应用接收数据(交易系统 - React)

// src/App.jsx(交易系统)
import { useEffect, useState } from 'react';

function App() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    // 获取主应用传递的数据
    const data = window.microApp?.getData();
    if (data) {
      setUser(data.user);
      console.log('收到主应用数据', data);
    }

    // 监听主应用数据变化
    const dataListener = (data) => {
      console.log('主应用数据更新', data);
      setUser(data.user);
    };

    window.microApp?.addDataListener(dataListener);

    return () => {
      window.microApp?.removeDataListener(dataListener);
    };
  }, []);

  return (
    <div>
      <h1>交易系统</h1>
      {user && <p>当前用户:{user.name}</p>}
    </div>
  );
}

export default App;

子应用接收数据(资管系统 - Vue 3)

<!-- src/App.vue(资管系统) -->
<template>
  <div>
    <h1>资管系统</h1>
    <p v-if="user">当前用户:{{ user.name }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const user = ref(null);

onMounted(() => {
  // 获取主应用传递的数据
  const data = window.microApp?.getData();
  if (data) {
    user.value = data.user;
    console.log('收到主应用数据', data);
  }

  // 监听主应用数据变化
  window.microApp?.addDataListener(dataListener);
});

function dataListener(data) {
  console.log('主应用数据更新', data);
  user.value = data.user;
}

onUnmounted(() => {
  window.microApp?.removeDataListener(dataListener);
});
</script>

运行效果

当用户访问 /trading 路由时,最终的 HTML 结构如下:

<div class="main-app">
  <nav>
    <a href="/trading">交易系统</a>
    <a href="/asset">资管系统</a>
  </nav>

  <!-- micro-app 是一个自定义元素 -->
  <micro-app name="trading" url="http://localhost:3001">
    <!-- 子应用的内容被渲染在这里 -->
    <!-- micro-app 默认通过 CSS 前缀实现样式隔离,不是真正的 Shadow DOM -->
    <div data-micro-app="trading">
      <h1>交易系统</h1>
      <p>当前用户:张三</p>
    </div>
  </micro-app>
</div>

从代码量和改造成本来看,micro-app 确实比 qiankun 简单不少。主应用只需要用标签嵌入,子应用几乎不用改。

3.4 micro-app 的优缺点

micro-app 的优点

1. 接入成本极低

  • 主应用:一行 microApp.start(),然后用标签嵌入子应用
  • 子应用:几乎不用改代码,配置跨域就行
  • 不需要改 Webpack 配置,不需要导出生命周期函数

2. 使用方式直观

  • 像使用 HTML 标签一样使用子应用
  • 通过 data 属性传递数据,通过事件接收消息
  • 学习成本低,团队容易上手

3. 样式隔离更彻底

  • 默认通过 CSS 前缀实现样式隔离,也支持开启 Shadow DOM
  • 配置简单,不需要额外的沙箱代码

4. 技术栈无关

  • 和 qiankun 一样,支持任意技术栈
  • React、Vue、Angular、jQuery 都可以
micro-app 的局限

1. 本质上还是运行时加载

  • 和 qiankun 一样,子应用是在浏览器里动态加载的
  • 首次加载子应用有延迟,用户能感知到加载过程
  • 运行时需要额外的沙箱和样式隔离代码

2. 还是没有类型安全

  • 主应用和子应用之间的数据传递没有类型检查
  • IDE 无法跳转到子应用的代码
  • 重构时容易遗漏,只能在运行时发现问题

3. 样式隔离有局限

  • 默认的 CSS 前缀隔离方案不是 100% 隔离
  • 如果开启 Shadow DOM,某些第三方库可能不兼容
  • 弹窗、下拉菜单等组件可能需要特殊处理

4. 社区相对较小

  • 比 qiankun 出来得晚,社区生态不如 qiankun
  • 遇到问题可能不太容易找到解决方案
micro-app 适用场景

适合用 micro-app 的场景

  • 想快速接入微前端,不想改太多代码
  • 团队微前端经验不多,想降低学习成本
  • 对接入成本比较敏感的项目

不适合用 micro-app 的场景

  • 对性能要求极高(运行时加载有开销)
  • 需要强类型安全和 IDE 支持
  • 使用了大量不兼容 Shadow DOM 的第三方库

3.5 qiankun vs micro-app:怎么选?

说了这么多,qiankun 和 micro-app 到底选哪个?简单总结一下:

对比总结

选 qiankun:团队有微前端经验,愿意花时间改造项目,需要成熟稳定的方案。

选 micro-app:想快速接入,不想改太多代码,团队微前端经验不多。

共同的问题:都是运行时加载,性能有开销,没有类型安全。

那有没有一种方案,既能享受微前端的好处,又能解决运行时加载和类型安全的问题?

有的。下一部分我们来聊聊 Module Federation

四、Module Federation:终于等到你

4.1 用了 qiankun/micro-app 之后的困惑

我之前用 qiankun 搭建了金融平台的微前端架构,5 个子应用(交易、资管、开户、做市、风控)跑起来了,但用了一段时间后,我发现了一些让我很难受的问题。

先说明一下:qiankun 和 micro-app 是两个独立的微前端框架,但它们的设计思路相似——都是「运行时加载整个子应用」。下面说的问题,两个框架都存在。

问题一:加载太重,React 加载了好几份

我们 5 个子应用里有 3 个用 React(交易、做市、风控),2 个用 Vue(资管用 Vue 3、开户用 Vue 2)。每个子应用打包后都包含一份完整的框架代码。用户访问首页,加载主应用 + 交易系统,光 React 就加载了 2 份。

这不是 bug,这是 qiankun/micro-app 的设计决定的——它们把子应用当成「独立网页」来加载,自然每个子应用都要带上自己的依赖。

实际数据
  • React + ReactDOM 压缩后约 140KB
  • 主应用 + 3 个 React 子应用 = 4 份 React = 560KB
  • Vue 3 约 90KB,Vue 2 约 80KB
  • 如果能共享,React 只需要 140KB
  • 浪费了 420KB+ 带宽和加载时间

问题二:只想用一个组件,却要加载整个子应用

有个需求:在首页 Dashboard 上展示交易系统的「快捷下单」组件和风控系统的「风险预警」组件。

用 qiankun 怎么做?

// qiankun:手动加载子应用到指定容器
loadMicroApp({
  name: 'trading',
  entry: 'http://localhost:3001',
  container: '#trading-container',  // 只想要一个组件,却要加载整个交易系统
});

loadMicroApp({
  name: 'risk',
  entry: 'http://localhost:3005',
  container: '#risk-container',     // 只想要一个预警组件,却要加载整个风控系统
});

用 micro-app 怎么做?

<!-- micro-app:用标签加载 -->
<micro-app name="trading" url="http://localhost:3001" />
<micro-app name="risk" url="http://localhost:3005" />

两个小组件,加载了 2 个完整应用,总共 4MB+ 的 JS。这合理吗?

问题三:子应用强依赖主应用

qiankun/micro-app 的子应用必须通过主应用的「坑位」才能渲染。如果我想让交易系统独立访问,或者让资管系统引用交易系统的组件,做不到。

问题四:没有类型安全

主应用传给子应用的数据没有类型检查。改了 user 对象的字段名,TypeScript 不会报错,只能在运行时发现问题。

4.2 qiankun/micro-app 的加载流程

先看看 qiankun/micro-app 是怎么加载子应用的:

sequenceDiagram participant 用户 participant 主应用 participant 子应用服务器 participant 浏览器 用户->>主应用: 访问 /trading 主应用->>主应用: 路由匹配,触发加载 主应用->>子应用服务器: fetch index.html 子应用服务器-->>主应用: 返回 HTML 主应用->>主应用: 解析 HTML,提取 JS/CSS 主应用->>子应用服务器: fetch app.js (包含 React + 业务代码) 子应用服务器-->>主应用: 返回 JS (约 800KB) 主应用->>浏览器: 创建沙箱,执行 JS 浏览器->>浏览器: 子应用渲染到容器

问题很明显:每次都要加载完整的子应用 JS,包括重复的 React

4.3 我理想中的微前端

如果有一种方案,能做到这些就好了:

  • 按需加载:我只想用「快捷下单」组件,就只加载这个组件的代码
  • 共享依赖:React 只加载一份,5 个子应用共用
  • 独立运行:子应用可以独立跑,也可以被其他应用引用
  • 类型安全:import 的时候有类型提示,重构不怕漏改

这就是 Module Federation 的设计理念。

4.4 Module Federation 是什么

Module Federation(模块联邦)是 Webpack 5 引入的功能,Vite 通过 @originjs/vite-plugin-federation 插件也支持了。本文的示例都基于 Vite,如果你用 Webpack 5,配置方式略有不同但原理一样。

它的核心思想是:

核心理念

让不同的应用可以共享模块,就像 npm 包一样,你可以 import 远程应用暴露的组件。

看看 Module Federation 的加载流程:

sequenceDiagram participant 用户 participant 主应用 participant 远程应用 participant 浏览器 用户->>主应用: 访问 Dashboard 主应用->>主应用: 需要「快捷下单」组件 主应用->>远程应用: 加载 remoteEntry.js (约 5KB) 远程应用-->>主应用: 返回模块元信息 主应用->>主应用: 检查依赖:React 已有,复用 主应用->>远程应用: 只加载 QuickOrder 组件 (约 20KB) 远程应用-->>主应用: 返回组件代码 浏览器->>浏览器: 渲染组件

对比一下:

加载对比
  • qiankun/micro-app:加载整个子应用 800KB+(包含重复的 React)
  • Module Federation:只加载需要的组件 25KB(复用已有的 React)

4.5 开发阶段:5 个子应用、10 个前端怎么协作

我们团队有 10 个前端开发,维护 5 个子应用。来看看不同方案的开发体验:

常规方案(npm 包共享)

  • 改了公共组件 → 发布 npm 包 → 每个项目升级版本 → 重新构建
  • 调试痛苦:改一行代码,要等 npm 发布,其他项目才能看到效果

qiankun/micro-app

  • 子应用独立开发,但调试时必须启动主应用
  • 想看效果?启动主应用 + 子应用,至少 2 个终端

Module Federation

  • 改了交易系统的组件 → 资管系统实时看到效果(热更新)
  • 不需要发 npm 包,不需要重新构建
  • 每个应用可以独立运行,也可以引用其他应用的模块
开发体验

Module Federation 的开发体验就像在一个 Monorepo 里开发,但代码可以分布在不同的仓库。改了组件,其他应用实时生效,不需要发包、不需要重启。

4.6 实战:金融平台微前端改造

现在用 Module Federation 重构我们的金融平台,5 个子应用:

  • 交易系统(React):暴露「快捷下单」「持仓列表」组件
  • 资管系统(Vue 3):暴露「净值曲线」「持仓分析」组件
  • 开户系统(Vue 2):暴露「开户表单」组件
  • 做市系统(React):暴露「报价面板」组件
  • 风控系统(React):暴露「风险预警」「风控报表」组件

交易系统配置(暴露组件)

// trading-system/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'trading',
      filename: 'remoteEntry.js',
      // 暴露组件给其他应用使用
      exposes: {
        './QuickOrder': './src/components/QuickOrder.jsx',
        './PositionList': './src/components/PositionList.jsx',
      },
      // 共享依赖,避免重复加载
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
  build: {
    target: 'esnext',
  },
});

风控系统配置(暴露组件)

// risk-system/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'risk',
      filename: 'remoteEntry.js',
      exposes: {
        './RiskAlert': './src/components/RiskAlert.jsx',
        './RiskReport': './src/components/RiskReport.jsx',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
  build: {
    target: 'esnext',
  },
});

主应用配置(引用远程组件)

// main-app/vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';

export default defineConfig({
  plugins: [
    react(),
    federation({
      name: 'main',
      // 声明远程应用
      remotes: {
        trading: {
          external: 'http://localhost:3001/assets/remoteEntry.js',
          format: 'esm',
        },
        risk: {
          external: 'http://localhost:3005/assets/remoteEntry.js',
          format: 'esm',
        },
        asset: {
          external: 'http://localhost:3002/assets/remoteEntry.js',
          format: 'esm',
        },
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
});

主应用使用远程组件

// main-app/src/pages/Dashboard.jsx
import { lazy, Suspense } from 'react';

// 动态导入远程组件,就像导入本地组件一样
const QuickOrder = lazy(() => import('trading/QuickOrder'));
const RiskAlert = lazy(() => import('risk/RiskAlert'));
const NetValueChart = lazy(() => import('asset/NetValueChart'));

function Dashboard() {
  return (
    <div className="dashboard">
      <h1>金融平台 Dashboard</h1>

      <div className="grid">
        {/* 交易系统的快捷下单组件 */}
        <Suspense fallback={<div>加载中...</div>}>
          <QuickOrder />
        </Suspense>

        {/* 风控系统的风险预警组件 */}
        <Suspense fallback={<div>加载中...</div>}>
          <RiskAlert />
        </Suspense>

        {/* 资管系统的净值曲线组件 */}
        <Suspense fallback={<div>加载中...</div>}>
          <NetValueChart />
        </Suspense>
      </div>
    </div>
  );
}

export default Dashboard;

看到了吗?同一个页面,展示了 3 个不同子应用的组件,每个组件按需加载,React 只有一份。

这是 qiankun/micro-app 做不到的——它们只能加载完整的子应用,不能只加载某个组件。

4.7 部署方案

Module Federation 的部署和普通前端项目一样,每个应用独立构建、独立部署。

Nginx 配置

# nginx.conf
server {
    listen 80;
    server_name finance.example.com;

    # 主应用
    location / {
        root /www/main-app/dist;
        try_files $uri $uri/ /index.html;
    }

    # 交易系统(远程模块)
    location /trading/ {
        alias /www/trading-system/dist/;
        add_header Access-Control-Allow-Origin *;
    }

    # 资管系统(远程模块)
    location /asset/ {
        alias /www/asset-system/dist/;
        add_header Access-Control-Allow-Origin *;
    }

    # 风控系统(远程模块)
    location /risk/ {
        alias /www/risk-system/dist/;
        add_header Access-Control-Allow-Origin *;
    }
}

CICD 流程

graph LR A[代码提交] --> B[CI 构建] B --> C[构建产物上传 CDN/服务器] C --> D[更新 remoteEntry.js] D --> E[其他应用自动获取最新模块]

每个子应用独立部署,更新后其他应用自动获取最新版本,不需要重新构建主应用。

4.8 Module Federation 的优缺点

优点
  • 按需加载:只加载需要的模块,不是整个应用
  • 共享依赖:React 等公共库只加载一份
  • 类型安全:可以共享 TypeScript 类型定义
  • 独立部署:每个应用独立构建、独立部署
  • 开发体验好:热更新,改了组件其他应用实时生效
缺点
  • 配置复杂:需要理解 shared、exposes、remotes 等概念
  • 版本管理:共享依赖的版本需要协调
  • 没有内置 CSS 沙箱:样式可能冲突,需要额外方案:
    • CSS Modules(推荐)
    • CSS-in-JS(styled-components、emotion)
    • Vite 插件(如 vite-plugin-css-prefix-auto,自动给选择器加作用域前缀)

4.9 三种方案对比总结

对比项 qiankun micro-app Module Federation
加载粒度 整个应用 整个应用 单个模块
依赖共享 不支持 不支持 支持
类型安全
JS 沙箱 有(Proxy) 有(模块作用域)
CSS 沙箱 有(前缀隔离) 无(需插件)
接入成本 中等 中等
性能 差(加载整个应用) 差(加载整个应用) 好(按需加载模块)
适合场景 整合独立系统 快速接入 模块共享、性能优先
选型建议
  • 选 qiankun:需要整合多个独立系统,技术栈不统一,需要沙箱隔离
  • 选 micro-app:想快速接入,不想改太多代码
  • 选 Module Federation:追求性能,需要模块级共享,团队技术栈统一

五、总结

微前端不是银弹,选型要根据实际场景:

  • 如果你的项目是多个独立系统拼接,技术栈不统一,选 qiankun
  • 如果你想快速接入,不想改太多代码,选 micro-app
  • 如果你追求性能和开发体验,团队技术栈统一,选 Module Federation

我个人更推荐 Module Federation,因为它的设计理念更先进——不是把应用「拼」在一起,而是让应用之间可以「共享模块」。如果这还不算是微前端的未来方向,什么才是呢?