Micro Frontends 技术选型思考

10 阅读8分钟

一、概述

Micro Frontends 将一个大型前端应用拆分为多个独立开发、独立部署的子应用,最终由主应用统一集成。核心实现方案分为两大类:

  • 构建时集成:打包阶段合并,产物是一个整体
  • 运行时集成:浏览器阶段动态加载,各子应用独立部署

二、方案一:构建时集成(npm 包)

原理

将子应用打包成 npm 包发布,主应用作为普通依赖引入。

// 子应用:发布为 @company/micro-app
export default function MicroApp() {
  return <div>Micro App Content</div>
}
// 主应用:像普通组件一样引入
import MicroApp from '@company/micro-app'

function App() {
  return (
    <div>
      <MicroApp />
    </div>
  )
}

适用场景

  • 子应用更新频率低,版本稳定
  • 团队技术栈完全统一(同框架同版本)
  • 对隔离性要求不高,更看重集成简单

优缺点

✅ 接入零改造❌ 子应用更新需主应用重新构建发布
✅ 无运行时开销❌ 无法独立部署,违背 MF 核心价值
✅ TS 类型天然共享❌ 各子应用依赖版本需严格对齐

⚠️ 注意:构建时集成本质上还是一个整体应用,严格意义上不算微前端,更适合做共享组件库而非子应用集成。


三、方案二:运行时集成 — Single-SPA

原理

Single-SPA 是一个微前端框架,主应用根据路由规则决定激活哪个子应用,子应用暴露标准生命周期钩子。

import { registerApplication, start } from 'single-spa'

// 注册子应用:指定名称、加载函数、激活规则
registerApplication({
  name: 'vue-app',
  app: () => System.import('@company/vue-app'), // 动态加载
  activeWhen: '/vue',                            // 路由匹配时激活
})

registerApplication({
  name: 'react-app',
  app: () => System.import('@company/react-app'),
  activeWhen: '/react',
})

start() // 启动路由监听
// 子应用:暴露 single-spa 标准生命周期
export const bootstrap = async () => { /* 初始化 */ }
export const mount = async (props) => { render(props.domElement) }
export const unmount = async () => { unmountReact() }

适用场景

  • 多团队、多框架并存(React + Vue + Angular)
  • 需要路由级别的子应用切换
  • 希望使用框架无关的标准协议

优缺点

✅ 框架无关,官方支持 React/Vue/Angular❌ 配置繁琐,学习曲线较陡
✅ 生命周期完善,有成熟生态❌ 无内置 CSS/JS 沙箱,需自行处理隔离
✅ 可配合 SystemJS 实现模块懒加载❌ 国内文档资源较少(推荐用 qiankun 替代)

💡 qiankun 是基于 Single-SPA 的封装,在其基础上增加了 JS 沙箱和 CSS 隔离,国内项目更推荐直接用 qiankun。


三(补充):qiankun

与 Single-SPA 的关系

qiankun = Single-SPA 的路由调度 + 开箱即用的沙箱 + 更简单的接入方式。

能力Single-SPAqiankun
路由调度 / 生命周期✅ 继承
JS 沙箱✅ Proxy 沙箱
CSS 隔离✅ 两种模式
HTML Entry(直接填 URL)❌ 需手动配 JS Entry
子应用预加载✅ prefetch
父子通信❌ 需自行实现initGlobalState

接入示例

import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'vue-app',
    entry: '//localhost:7101',          // 直接填子应用 URL,qiankun 自动解析 HTML
    container: '#subapp-viewport',
    activeRule: '/vue',
    props: { token: getToken() },       // 向子应用传参
  },
])

start({ prefetch: true })               // 开启预加载
// 子应用暴露生命周期(与 single-spa 一致)
export async function bootstrap() {}
export async function mount(props) {
  render(props)
}
export async function unmount() {
  instance.$destroy()
}

JS 隔离 — Proxy 沙箱

qiankun 为每个子应用创建一个 Proxy 对象代理 window,子应用所有全局变量的读写都发生在这个假 window 上,子应用卸载时直接丢弃,不污染真实 window

// qiankun 内部简化原理
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
  set(target, key, value) {
    target[key] = value       // 写入假 window,不影响真实 window
    return true
  },
  get(target, key) {
    return key in target ? target[key] : window[key]  // 读不到再去真 window 找
  },
})
// 子应用运行时,其访问的 window 均指向 proxy

CSS 隔离 — 两种模式

模式一:Shadow DOMstrictStyleIsolation: true

将子应用挂载节点包在 Shadow DOM 内,样式天然不穿透边界。

start({ strictStyleIsolation: true })

❌ 缺点:子应用内弹窗(Modal/Drawer)通常挂载到 document.body,会逃出 Shadow DOM 导致样式丢失。

模式二:Scoped CSSexperimentalStyleIsolation: true

qiankun 动态给子应用所有 CSS 选择器加上属性前缀,类似 Vue Scoped CSS 的实现。

/* 子应用原始样式 */
.button { color: red; }

/* qiankun 处理后 */
div[data-qiankun="app-name"] .button { color: red; }
start({ experimentalStyleIsolation: true })

❌ 缺点:动态插入的样式(如 antd 弹窗、第三方 SDK 注入的 style 标签)覆盖不到,需手动处理。

优缺点

✅ 框架无关,多框架并存首选❌ CSS 沙箱均有边界情况,弹窗样式需额外处理
✅ JS/CSS 双沙箱,隔离性强❌ 子应用需改造入口,暴露生命周期
✅ HTML Entry 接入简单,国内生态成熟❌ 路由切换时 mount/unmount 有性能开销

四、方案三:Module Federation(Webpack 5)

原理

Webpack 5 内置能力,允许一个应用在运行时动态加载另一个应用暴露的模块,并共享依赖。

// webpack.config.js  主应用(消费者)
new ModuleFederationPlugin({
  name: 'shell',
  remotes: {
    // 声明远程应用及其入口地址
    mfApp: 'mfApp@http://localhost:3001/remoteEntry.js',
  },
  shared: {
    react: { singleton: true },       // 共享 React,避免多实例
    'react-dom': { singleton: true },
  },
})
// webpack.config.js — 子应用(提供者)
new ModuleFederationPlugin({
  name: 'mfApp',
  filename: 'remoteEntry.js',         // 暴露入口文件
  exposes: {
    './Component': './src/Component', // 暴露的模块路径
    './App': './src/App',
  },
  shared: {
    react: { singleton: true },
    'react-dom': { singleton: true },
  },
})
// 主应用中使用远程模块(运行时动态加载)
const RemoteComponent = React.lazy(() => import('mfApp/Component'))

function App() {
  return (
    <Suspense fallback="Loading...">
      <RemoteComponent />
    </Suspense>
  )
}

适用场景

  • 技术栈统一(全 React 或全 Vue),Webpack 5 体系
  • 需要共享依赖、降低总包体积
  • 追求最佳开发体验,子应用独立部署

优缺点

✅ 原生共享依赖,包体积最优❌ 强依赖 Webpack 5,Vite 需额外插件
✅ 运行时加载,独立部署互不影响❌ JS 沙箱隔离弱,全局污染需自行处理
✅ 类型共享可通过插件实现❌ 版本不一致时 singleton 报错较难排查

五、方案四:Web Components

原理

用浏览器原生 Custom Elements API 将子应用封装为自定义 HTML 标签,天然支持 Shadow DOM 样式隔离。

// 子应用:定义 Custom Element
class MicroFrontend extends HTMLElement {
  connectedCallback() {
    // 元素挂载到 DOM 时触发
    const shadow = this.attachShadow({ mode: 'open' }) // Shadow DOM 样式隔离
    shadow.innerHTML = `
      <style>
        /* 样式只作用于 Shadow DOM 内部 */
        .container { color: red; }
      </style>
      <div class="container">Micro Frontend Content</div>
    `
  }

  disconnectedCallback() {
    // 元素从 DOM 移除时触发,做清理
  }
}

// 注册为全局自定义标签
customElements.define('micro-frontend', MicroFrontend)
<!-- 主应用:像普通 HTML 标签一样使用 -->
<micro-frontend data-user-id="123"></micro-frontend>
// 与子应用通信:通过 Attribute 或 CustomEvent
document.querySelector('micro-frontend').setAttribute('data-token', token)

// 子应用向外发事件
this.dispatchEvent(new CustomEvent('navigate', { detail: { path: '/home' }, bubbles: true }))

适用场景

  • 希望完全框架无关,与 React/Vue/Angular 均能集成
  • 需要 Shadow DOM 提供的原生 CSS 隔离
  • 构建可复用的跨团队、跨框架 UI 组件

优缺点

✅ 浏览器原生标准,零框架依赖❌ 与 React/Vue 深度集成仍有摩擦(事件系统不同)
✅ Shadow DOM 提供天然 CSS 隔离❌ SSR 支持差(Declarative Shadow DOM 仍在普及中)
✅ 生命周期由浏览器管理❌ 内部状态管理、路由需自行实现

六、方案五:iframe

原理

最古老也最彻底的隔离方案,每个子应用运行在独立的浏览上下文中,JS/CSS/DOM 完全隔离。

<!-- 主应用:嵌入子应用 -->
<iframe
  src="http://micro-app.example.com"
  sandbox="allow-scripts allow-same-origin allow-forms"
  style="width: 100%; height: 100%; border: none;"
></iframe>
// 主 → 子通信(postMessage)
const iframe = document.querySelector('iframe')
iframe.contentWindow.postMessage(
  { type: 'USER_INFO', payload: { userId: 123, token } },
  'http://micro-app.example.com' // 指定 origin,安全关键
)

// 子 → 主通信
window.parent.postMessage({ type: 'NAVIGATE', path: '/home' }, '*')

// 主应用监听消息
window.addEventListener('message', (event) => {
  // 必须校验来源
  if (event.origin !== 'http://micro-app.example.com') return
  handleMessage(event.data)
})
// 解决刷新后路由丢失:将子路由同步到主应用 URL
window.addEventListener('message', (event) => {
  if (event.data.type === 'ROUTE_CHANGE') {
    const url = new URL(window.location.href)
    url.searchParams.set('subRoute', event.data.path)
    window.history.replaceState({}, '', url)
  }
})

适用场景

  • 嵌入第三方不可控应用(外部供应商系统)
  • 有安全合规要求,必须强隔离
  • 快速集成老系统,子应用完全不需改造

优缺点

✅ 最强隔离,子应用零改造❌ 通信复杂,需设计消息协议
✅ URL/Cookie/Storage 完全独立❌ 弹窗无法突破 iframe 边界
✅ 安全性最高,sandbox 可细粒度控制❌ 性能最差,独立渲染进程

七、方案对比总表

维度构建时(npm)Single-SPAModule FederationWeb Componentsiframe
集成时机构建时运行时运行时运行时运行时
JS 隔离❌ 弱⚠️ 部分✅ 最强
CSS 隔离✅ Shadow DOM✅ 最强
独立部署
共享依赖⚠️ 手动✅ 原生
框架无关⚠️
接入改造成本极低
适用场景组件库共享多框架大型平台统一技术栈平台跨框架组件第三方/强隔离

八、选型决策树

子应用是否来自第三方 / 有强安全隔离要求?
├── 是 → iframe
└── 否
    ├── 是否只是共享 UI 组件,不需要独立部署?
    │   └── 是 → 构建时集成(npm 包)
    └── 否(需要独立部署)
        ├── 多框架并存(React + Vue + 老系统)?
        │   └── 是 → Single-SPA / qiankun
        └── 否
            ├── 技术栈统一 + Webpack 5 体系?
            │   └── 是 → Module Federation
            └── 追求原生标准 / 跨框架组件复用?
                └── 是 → Web Components

九、常见陷阱

  1. 构建时集成被误当微前端:npm 包方案没有独立部署能力,不是真正的微前端
  2. CSS 全局污染:未使用沙箱方案时,子应用的 :root 变量、全局样式会互相覆盖
  3. React 多实例问题:Module Federation 未配置 singleton: true 时,hooks 会抛出错误
  4. iframe 弹窗穿透:子应用内的 Modal/Drawer 挂载到 document.body,在 iframe 中无法全屏显示
  5. postMessage 安全漏洞:接收消息时未校验 event.origin,导致跨站消息注入