CC 咖啡猫的工作空间 Coding Space

微前端 — Micro Frontend

1. 核心概念与动机

定义

微前端是将前端应用拆分为多个独立子应用,每个子应用可独立开发、测试、部署,运行时通过容器动态聚合为完整应用。核心原则:技术栈无关、独立仓库、独立生命周期、自治团队。

解决的问题

问题 具体表现
巨石应用难维护 代码量数十万行、多团队 Git 冲突频繁、构建超 5 分钟
技术栈锁定 老项目无法渐进升级(jQuery → Vue → React),新功能被迫沿用旧框架
跨团队协作瓶颈 发布排期耦合、feature flag 膨胀、Code Review 跨域困难
独立交付受阻 一个小组的 bug 阻塞全应用发布、回滚范围过大

适用场景

  • 后台管理系统:多业务线聚合(CMS/订单/用户/支付各自独立团队)
  • 大型 C 端应用:多团队并行迭代(首页/搜索/详情/个人中心独立发布)
  • 渐进式技术改造:新功能用新框架(React 18 / Vue 3),老系统保持不动
  • B2B 可组合平台:不同客户选择不同模块组合(如电商平台功能开关)

2. 主流方案详解

2.1 qiankun(阿里,基于 single-spa)

架构原理

主应用 ──注册→ 子应用列表(activeRule ↔ name ↔ entry)
   │
   ├─ 路由变化匹配 activeRule
   ├─ HTML Entry 加载子应用 HTML → 解析出 JS/CSS
   ├─ 创建 JS 沙箱 + CSS 隔离
   ├─ 执行生命周期: bootstrap → mount → unmount → update
   └─ 渲染到指定 DOM 容器 (#app)

主应用注册代码

// apps/main/src/micro/index.ts
import { registerMicroApps, start, initGlobalState } from 'qiankun';
import type { ObjectType } from '@qiankunjs/core';

interface AppConfig {
  name: string;
  entry: string;          // 子应用 HTML 地址
  container: string;      // 挂载 DOM 选择器
  activeRule: string;     // 路由前缀匹配
  props?: ObjectType;     // 传递给子应用的数据
}

const apps: AppConfig[] = [
  {
    name: 'app-order',
    entry: '//localhost:3001',
    container: '#sub-app-container',
    activeRule: '/order',
    props: { baseUrl: '/order' },
  },
  {
    name: 'app-user',
    entry: '//localhost:3002',
    container: '#sub-app-container',
    activeRule: '/user',
  },
];

registerMicroApps(apps, {
  beforeLoad: [async (app) => console.log('[qiankun] before load', app.name)],
  beforeMount: [async (app) => console.log('[qiankun] before mount', app.name)],
  afterMount: [async (app) => console.log('[qiankun] after mount', app.name)],
  beforeUnmount: [async (app) => console.log('[qiankun] before unmount', app.name)],
  afterUnmount: [async (app) => console.log('[qiankun] after unmount', app.name)],
});

// 启动(可配置 prefetch / sandbox / singular 等)
start({ sandbox: { experimentalStyleIsolation: true }, prefetch: 'all' });

子应用入口(以 Umi 为例)

// apps/order/src/app.ts
export const qiankun = {
  async bootstrap() {
    console.log('order app bootstraped');
  },
  async mount(props: any) {
    render(props); // props 含 container/onGlobalStateChange/setGlobalState
  },
  async unmount(props: any) {
    ReactDOM.unmountComponentAtNode(props.container ? props.container.querySelector('#root')! : document.getElementById('root'));
  },
};

生命周期

beforeLoad → bootstrap → beforeMount → mount → afterMount
                                    ↓ (路由失活)
                      beforeUnmount → unmount → afterUnmount

CSS 隔离

模式 实现方式 优缺点
experimentalStyleIsolation 每个子应用包裹 Shadow DOM 兼容性问题(弹窗/Dialog 等 appendToBody 会逃逸)
strictStyleIsolation Scoped CSS(给选择器加 div[data-qiankun-appName] 前缀) 不防动态插入的 style 标签,性能损耗
自定义方案 postcss-prefix-selector + BEM 需要构建配合,兼容性好

JS 沙箱

沙箱类型 原理 兼容性 性能
ProxySandbox ES6 Proxy 代理 window 对象,激活/失活时切换虚拟 window 与真实 window IE11 不支持
SnapshotSandbox 激活时保存 window 快照 → 失活时遍历恢复差异 全兼容 低(大量对象遍历)
LegacySandbox 单实例 Proxy 沙箱(仅全局 window 代理) 同上 Proxy 中等
// SnapshotSandbox 简版示意
class SnapshotSandbox {
  private snapshot: Record<string, any> = {};
  private modifiedProps: Record<string, any> = {};

  activate() {
    this.snapshot = {};
    for (const key in window) {
      this.snapshot[key] = window[key]; // 保存快照
    }
    Object.assign(window, this.modifiedProps); // 恢复上次修改
  }

  deactivate() {
    for (const key in window) {
      if (window[key] !== this.snapshot[key]) {
        this.modifiedProps[key] = window[key]; // 记录差异
        window[key] = this.snapshot[key];       // 恢复快照
      }
    }
  }
}

通信机制:initGlobalState

// 主应用
const { onGlobalStateChange, setGlobalState } = initGlobalState({ user: null, token: '' });

onGlobalStateChange((state, prev) => {
  console.log('[qiankun] global state changed', state, prev);
});

setGlobalState({ user: { id: 1, name: 'admin' }, token: 'xxx' });

// 子应用
props.onGlobalStateChange((state, prev) => { /* 同步 */ }, true); // true = 立即触发
props.setGlobalState({ /* 局部更新 */ });

预加载 prefetch

start({
  prefetch: 'all',           // 所有子应用
  prefetch: ['app-order'],   // 指定列表
  prefetch: true,            // 第一个子应用加载完后预加载其余
});

常见坑

问题 解决方案
子应用路由前缀 activeRule: '/order' + 子应用 router base: /order
静态资源 404 __webpack_public_path__ 动态设置:window.__POWERED_BY_QIANKUN__ && __webpack_public_path__
antd/Element UI 样式冲突 开启 experimentalStyleIsolation 或主应用/子应用各用不同版本且 CSS 加前缀
子应用弹窗位置错乱 Dialog/Modal 的 getContainer 指定为子应用容器节点
全局事件污染 unmount 时清理 addEventListener / setInterval

2.2 Module Federation(Webpack 5 / Rspack)

核心概念

角色 说明
Host 消费者,运行时加载远程模块
Remote 生产者,暴露模块供 Host 使用
exposes Remote 声明哪些模块对外暴露
remotes Host 声明从哪个 Remote 加载模块
shared 共享依赖(React/Vue/Lodash),避免重复加载
singleton 强制单例(防止多 React 实例导致 hooks 报错)
remoteEntry.js Remote 的加载入口文件,包含模块映射清单

配置详解

// apps/remote/webpack.config.js —— Remote 生产者
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  output: { publicPath: 'http://localhost:3001/' },
  plugins: [
    new ModuleFederationPlugin({
      name: 'order',
      filename: 'remoteEntry.js',
      exposes: {
        './OrderList': './src/pages/OrderList',
        './OrderDetail': './src/pages/OrderDetail',
        './store': './src/store',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
        'react-router-dom': { singleton: true },
      },
    }),
  ],
};
// apps/host/webpack.config.js —— Host 消费者
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'shell',
      remotes: {
        order: 'order@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
};
// apps/host/src/App.tsx —— 使用 Remote 模块
const OrderList = React.lazy(() => import('order/OrderList'));

function App() {
  return (
    <React.Suspense fallback={<Spin />}>
      <OrderList />
    </React.Suspense>
  );
}

加载原理

Host 运行时执行:
  import('order/OrderList')
    → 动态创建 <script src="http://localhost:3001/remoteEntry.js">
    → remoteEntry.js 自执行,在 __webpack_modules__ 注册所有 exposes 模块
    → 检查 shared 作用域: react/react-dom 版本是否满足 → 不满足则加载 Remote 的副本
    → 返回 OrderList 模块 → Host 渲染

shared 关键属性

shared: {
  react: {
    singleton: true,       // 强制全局只有一个 React 实例
    requiredVersion: '^18.0.0', // 版本约束,不满足时 console.warn
    eager: true,           // 立即加载(非懒加载,首屏可用)
    strictVersion: true,   // 版本不匹配时直接抛错
    shareScope: 'default', // 共享作用域名称,多组隔离用
  },
}

Module Federation vs qiankun 本质区别

维度 Module Federation qiankun
组合粒度 模块级(组件/工具/Hooks) 应用级(完整子应用,路由维度)
仓库关系 多个模块可同仓库,也可跨仓库 每个子应用独立仓库
构建紧密度 构建时需知道 remote 地址 运行时通过 HTML Entry 发现
技术栈绑定 共享 same JS 运行时(必须 webpack 5+) 不限(HTML Entry 加载裸 HTML)
部署关联 Remote 更新后 Host 需重新构建共享?实际运行时加载最新 remoteEntry 子应用独立部署,主应用自动获取最新
沙箱隔离 无(靠 shared singleton 约定) Proxy/快照沙箱 + CSS 隔离
适用场景 微模块/组件共享/Monorepo 补充 微前端应用组合

Rspack Module Federation

Rspack 的 MF 实现与 Webpack MF 配置完全兼容,仅需替换插件包名:

// rspack.config.js —— 与 webpack 配置几乎一致
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack');
// 或 rspack 内置: rspack.ModuleFederationPlugin

优势:构建速度提升 5-10x(Rust 编写)、HMR 更快、配置对齐 Webpack 生态。

常见坑

问题 解决方案
React 多实例(hooks 报错) shared: { react: { singleton: true } } 必须两端配置一致
Remote 加载失败 React.lazy + ErrorBoundary 包裹:
CSS 重复加载 shared 无法共享 CSS;需写 CSS 约定(BEM/CSS Modules)或用 tailwind
shared 版本不匹配静默 strictVersion: true 在构建时报错
// 降级处理
function OrderPage() {
  return (
    <ErrorBoundary fallback={<Alert message="订单模块加载失败,请刷新重试" type="error" />}>
      <React.Suspense fallback={<Skeleton active />}>
        <OrderList />
      </React.Suspense>
    </ErrorBoundary>
  );
}

2.3 wujie(腾讯)

核心思路

iframe + Web Component 混合方案。子应用运行在 iframe 中(天然硬隔离),通过 Web Component 自定义元素挂载到主应用 DOM 树。

主应用 DOM
  └─ <wujie-app name="order" url="http://localhost:3001">
       └─ #shadow-root (Web Component)
            └─ <iframe srcdoc="...">
                 └─ 子应用完整运行环境(window/document/location 全部隔离)

使用方式

// 主应用
import { Wujie } from 'wujie-react';

function App() {
  const [show, setShow] = useState(true);
  return (
    <Wujie
      name="order"
      url="//localhost:3001"
      alive={true}                    // 保活模式(keep-alive,切回时不重新加载)
      props={{ user: { id: 1 } }}     // 传递数据
      sync={true}                     // 同步路由
      fetch={customFetch}             // 自定义 fetch(用于鉴权 header 注入)
      beforeLoad={/* ... */}
      beforeMount={/* ... */}
    />
  );
}

硬隔离 vs 软隔离

维度 wujie(硬隔离) qiankun(软隔离)
JS 隔离 iframe 天然隔离,window/document/location 完全独立 Proxy 沙箱模拟隔离,仍有逃逸风险(如 Object.defineProperty
CSS 隔离 iframe 天然隔离 Scoped CSS / Shadow DOM,弹窗/Dialog 易逃逸
Cookie/LocalStorage 完全隔离(可通过 postMessage 同步) 共享主应用(需手动隔离 key 命名空间)
通信开销 postMessage(异步,序列化) 全局状态同步(同步,引用传递)
子应用改造量 几乎为零(无需改源码) 需要入口文件暴露生命周期
安全风险 低(iframe 沙箱 + sandbox 属性) 中(Proxy 沙箱有已知逃逸漏洞)

通信

// props 传参
<Wujie name="order" url="..." props={{ token: 'xxx' }} />

// 主 → 子
wujieBus.$emit('order:refresh', { id: 1 });

// 子 → 主(子应用内)
window.$wujie?.bus.$emit('global:logout');
// 或
window.$wujie?.props.methods.someCallback(data);

2.4 micro-app(京东)

核心思路

类 Web Component 思想,使用 CustomElement + Shadow DOM 接入子应用。接入成本极低,类似 iframe 的无缝替换体验。

<!-- 主应用直接使用自定义标签加载子应用 -->
<micro-app
  name="order"
  url="http://localhost:3001"
  baseroute="/order"
  disabledScopecss={false}
  iframe={false}
></micro-app>

子应用

// 子应用入口处(如 main.js)
if (window.__MICRO_APP_ENVIRONMENT__) {
  // 微前端环境下设置 publicPath 和路由 base
  __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__;
}

生命周期

事件 说明
created 元素创建
beforemount 开始加载
mounted 加载完成并渲染
unmount 卸载
error 加载失败
document.querySelector('micro-app[name="order"]')?.addEventListener('mounted', (e) => {
  console.log('order app mounted', e);
});

通信

// 主 → 子
microApp.setData('order', { type: 'newMsg', data: { hello: 'world' } });

// 子 → 主
window.microApp?.dispatch({ type: 'logout' });

特点

  • 接入成本最低:一行自定义标签即可加载子应用
  • Shadow DOM 可选:开启后 CSS 隔离效果好,但兼容 antd 等 appendToBody 弹窗需配置
  • iframe 降级iframe={true} 可回退到 iframe 模式

2.5 四方案对比表

维度 qiankun Module Federation wujie micro-app
隔离粒度 应用级 模块级(组件/库) 应用级(硬隔离) 应用级
JS 沙箱 Proxy / 快照 共享作用域(无隔离) iframe 硬隔离 Proxy
CSS 隔离 实验性 / Scoped 前缀 无原生(需约定) iframe 硬隔离 Scoped CSS / Shadow DOM
通信机制 initGlobalState 共享模块 / shared postMessage / EventBus EventBus
子应用改造量 需暴露生命周期 零(组件级引用) 几乎为零 极少(仅 publicPath)
技术栈限制 不限(HTML Entry) 限同一构建体系(Webpack/Rspack) 不限(iframe) 不限
首屏性能 中(HTML Entry 解析开销) 优(模块级懒加载) 中(iframe 初始化开销)
兼容性 IE11(SnapshotSandbox) 现代浏览器 现代浏览器 现代浏览器
学习成本 (理解共享作用域概念)
社区活跃度 高(阿里,star 16k+) 极高(webpack 官方) 中(腾讯) 中(京东)
适用场景 后台系统 / 多团队并行 组件级共享 / 微模块架构 安全隔离要求高 / 老旧系统嵌入 快速接入 / 迁移

3. 关键实战问题

3.1 公共依赖处理

方案 原理 体积 隔离性 适用场景
externals(CDN) React/Vue/Lodash 通过 CDN 加载,全局共享 极小(单份) 弱(window.React 全局) 技术栈统一、部署简单
shared(MF) webpack shared scope 复用已加载模块 小(按版本分 chunk) 中(singleton 约束版本) 多应用 MF 共享
独立打包 每个子应用各自打包自己的依赖 大(N 份) 强(完全隔离) 技术栈不同、必须隔离
qiankun externals 主应用挂载全局 + 子应用不打入公共库 中(版本需一致) qiankun 体系

经验法则

  • 同框架同版本 → externals 或 shared(省体积)
  • 同框架不同版本 → shared singleton 限制版本范围,或各自打包(省心但费体积)
  • 不同框架 → 各自打包,无共享

3.2 样式隔离方案

方案 原理 兼容性 维护成本
BEM 命名约定 .order-list__item--active 加命名空间前缀 全兼容 高(需团队约定+review)
CSS Modules .module.css 编译生成哈希类名 全兼容 中(需构建支持)
CSS-in-JS styled-components / emotion 运行时隔离 全兼容 中(运行时开销)
Shadow DOM 浏览器原生样式隔离 部分(antd 弹窗逃逸) 低(框架自带)
postcss-prefix-selector 自动给所有选择器加前缀 .app-order 全兼容 低(构建自动处理)
// postcss.config.js —— 自动加前缀
module.exports = {
  plugins: [
    require('postcss-prefix-selector')({
      prefix: '.app-order',
      transform(prefix, selector, prefixedSelector) {
        // 保留 :root / :global 开头的选择器
        if (selector.startsWith(':root') || selector.startsWith(':global')) {
          return selector;
        }
        return prefixedSelector;
      },
    }),
  ],
};

3.3 登录态与鉴权

主应用
  ├─ 统一登录页面 → 获取 JWT Token
  ├─ Token 存入 localStorage(约定 key: AUTH_TOKEN)
  ├─ 子应用加载时通过 props / postMessage 透传
  └─ Token 刷新:主应用定时刷新后同步给所有子应用

问题点:
  ├─ Cookie SameSite 策略(Chrome 默认 Lax 会拦截 iframe 内 Cookie)
  ├─ SSO 单点登录(子应用各自跳转 SSO 时需统一回跳地址)
  └─ Token 过期同步(子应用检测 401 → 通知主应用刷新 → 重试请求)
// 主应用统一 token 刷新逻辑
let isRefreshing = false;
let failedQueue: Array<{resolve: Function; reject: Function}> = [];

function refreshToken(): Promise<string> {
  return axios.post('/api/auth/refresh', { refreshToken: storage.get('REFRESH_TOKEN') });
}

// axios 响应拦截器
axios.interceptors.response.use(
  (res) => res,
  async (error) => {
    if (error.response?.status !== 401) return Promise.reject(error);
    if (!isRefreshing) {
      isRefreshing = true;
      try {
        const { token } = await refreshToken();
        storage.set('AUTH_TOKEN', token);
        // 同步给所有子应用
        microApps.forEach((app) => app.setGlobalState?.({ token }));
        isRefreshing = false;
        failedQueue.forEach(({ resolve }) => resolve(token));
        failedQueue = [];
        return axios(error.config);
      } catch {
        isRefreshing = false;
        failedQueue.forEach(({ reject }) => reject(error));
        failedQueue = [];
        // 跳转登录页
      }
    } else {
      return new Promise((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      });
    }
  },
);

3.4 子应用缓存策略

策略 做法 效果
版本文件名 构建产出含 hash(main.a1b2c3.js 用户始终获取最新版本
HTML 缓存时间 HTML 设置 Cache-Control: no-cache 浏览器每次请求最新 HTML 入口
CDN 缓存 静态资源(JS/CSS/IMG)设置长期 max-age=31536000 + hash 缓存命中率高,版本切换自动失效
灰度发布 根据 userId/cookie 分流不同版本 小范围验证后全量
特性开关 通过 props 传递 featureFlag 动态控制功能显隐,无需重新部署
# HTML 入口不缓存
location /order {
  add_header Cache-Control "no-cache, must-revalidate";
  try_files $uri /order/index.html;
}

# 静态资源长期缓存
location /order/static {
  add_header Cache-Control "public, max-age=31536000, immutable";
}

3.5 错误处理与降级

// ErrorBoundary 包裹子应用容器
class SubAppErrorBoundary extends React.Component<{name: string}, {hasError: boolean}> {
  state = { hasError: false };

  static getDerivedStateFromError() {
    return { hasError: true };
  }

  componentDidCatch(error: Error) {
    console.error(`[SubApp] ${this.props.name} 渲染异常`, error);
    // 上报监控
  }

  render() {
    if (this.state.hasError) {
      return (
        <Result
          status="error"
          title={`${this.props.name} 模块加载异常`}
          extra={<Button onClick={() => this.setState({ hasError: false })}>重试</Button>}
        />
      );
    }
    return this.props.children;
  }
}
// qiankun 加载超时处理
registerMicroApps(apps, {
  beforeLoad: [async (app) => {
    const timeout = 10000;
    return Promise.race([
      Promise.resolve(),
      new Promise((_, reject) => setTimeout(() => reject(new Error(`加载超时 ${app.name}`)), timeout)),
    ]);
  }],
});

3.6 性能优化

优化项 做法 效果
预加载 主应用 idle 时预加载其他子应用 JS/CSS 路由切换瞬间渲染
预请求 子应用数据和 JS 并行加载(HTML Entry 提前解析 API 路径) 减少瀑布请求
按需加载 仅加载当前路由匹配的子应用 首屏体积最小
keep-alive wujie/micro-app 保活模式,子应用切换不销毁 切换零等待(内存消耗增加)
异步分包 MF 模式下按页面分包远程模块 粒度最细
// qiankun 预加载 + 手动控制
import { prefetchApps } from 'qiankun';

// 首屏加载完成后,空闲时预加载高频子应用
window.requestIdleCallback(() => {
  prefetchApps([
    { name: 'order', entry: '//localhost:3001' },
    { name: 'user', entry: '//localhost:3002' },
  ]);
});

4. 微前端 vs 替代方案

什么时候不需要微前端

  • 小团队(<10人):单体应用足够,微前端反而增加架构复杂度
  • 单一业务线:无多团队协作冲突,模块化 + Monorepo 即可
  • 技术栈统一:无渐进升级需求,Monorepo 共享 lint/CI/测试更高效
  • 构建速度未成瓶颈:5 分钟内的构建不值得引入运行时开销

替代方案对比

方案 优势 劣势 适用场景
Monorepo(pnpm workspace / turborepo) 代码共享简单、统一 lint/test/CI、原子提交 构建慢(随代码量线性增长)、部署耦合 技术栈统一、单团队
模块化拆分 代码层面解耦、tree-shaking 友好 统一构建部署、无法独立发布 代码组织阶段
iframe 天然硬隔离、零改造、接入最快 体验差(滚动/URL 不同步/弹窗溢出)、通信麻烦、SEO 不友好 嵌入第三方不可信内容、老系统兜底
Web Components 标准技术、框架无关 生态不成熟、SSR 难、复杂场景性能问题 UI 组件库跨框架

决策树

需要拆前端么?
├─ 小团队 / 单一业务 / 构建 < 3min → 不需要,Monorepo 或模块化即可
└─ 多团队 / 多业务线 / 构建 > 5min
   ├─ 是否要求独立部署?
   │  ├─ 否 → Monorepo + 模块化
   │  └─ 是
   │     ├─ 是否技术栈无关?
   │     │  ├─ 否(全部 React) → Module Federation(模块级)或 qiankun(应用级)
   │     │  └─ 是
   │     │     ├─ 安全隔离要求高?→ wujie(iframe 硬隔离)
   │     │     ├─ 接入成本优先?→ micro-app(一行标签)
   │     │     └─ 生态成熟优先?→ qiankun(社区资源最多)
   │     └─ 还需要考虑
   │        ├─ 组件级共享 > 应用级组合 → Module Federation
   │        └─ 需要 IE11 兼容 → qiankun(SnapshotSandbox)

5. 参考资源