Skip to content

微前端

什么是微前端

微前端(Micro Frontends)是一种将前端应用拆分为多个独立开发、独立部署、独立运行的小型应用的架构模式。它借鉴了后端微服务的理念,将巨石前端(Monolith Frontend)拆解为多个可组合的子应用,最终由一个主应用(容器应用)统一编排和加载。

微前端核心理念:

传统巨石前端                           微前端架构

┌──────────────────────┐       ┌──────────────────────────────┐
│                      │       │         主应用 (Main App)      │
│    巨大的单体应用      │       │  ┌─────────┬────────────────┐ │
│                      │       │  │ 导航栏   │   路由管理      │ │
│  所有团队共享一个仓库   │       │  ├─────────┴────────────────┤ │
│  所有功能耦合在一起     │  ═══▶ │  │                          │ │
│  技术栈统一且锁死       │       │  │  ┌──────┐ ┌──────┐      │ │
│  发布牵一发动全身       │       │  │  │子应用A│ │子应用B│ ...  │ │
│                      │       │  │  │ React │ │ Vue  │      │ │
│                      │       │  │  └──────┘ └──────┘      │ │
└──────────────────────┘       │  └──────────────────────────┤ │
                               └──────────────────────────────┘

微前端的核心价值在于让大型前端应用能够按业务域拆分,各团队对自己的子应用拥有完全的自治权——从技术选型、开发节奏到部署流程,都可以独立决策。

微前端解决的核心问题

问题域                    具体痛点                         微前端如何解决
─────────────────────────────────────────────────────────────────────────
巨石应用膨胀        代码量巨大、构建慢、维护难            按业务域拆分为独立子应用
技术栈锁定          无法渐进式升级、新技术无法引入          子应用技术栈独立,可用不同框架
团队协作瓶颈        多团队共享仓库、频繁冲突               各团队独立仓库、独立开发
发布耦合            一个模块改动需全量发布                 子应用独立部署、独立上线
遗留系统迁移        老系统无法一次性重写                   新老系统并存,渐进式迁移

微前端架构模式

微前端的实现方式有多种,每种方案在隔离性、接入成本、灵活性上各有取舍。

方案一:iframe

最古老也最简单的"微前端"方案,通过 <iframe> 嵌入子应用页面。

主应用
┌─────────────────────────────────┐
│  导航栏                          │
│  ┌───────────────────────────┐  │
│  │       <iframe>            │  │
│  │  ┌─────────────────────┐  │  │
│  │  │    子应用(独立页面)  │  │  │
│  │  │    完全独立的上下文    │  │  │
│  │  └─────────────────────┘  │  │
│  └───────────────────────────┘  │
└─────────────────────────────────┘

优点:天然隔离(JS / CSS / DOM 完全隔离),实现简单

缺点

  • 每次加载都需要重新初始化整个页面,性能差
  • URL 不同步,刷新后 iframe URL 丢失
  • 全局弹窗无法突破 iframe 边界
  • 与主应用通信困难,只能通过 postMessage

方案二:路由分发

通过 Nginx 或网关层按 URL 路径将不同路由分发到不同的独立应用。

浏览器请求


┌──────────────────┐
│   Nginx / 网关    │
│                  │
│  /app-a/*  ──────┼──▶  App A (React)   独立部署在 server-a
│  /app-b/*  ──────┼──▶  App B (Vue)     独立部署在 server-b
│  /app-c/*  ──────┼──▶  App C (Angular) 独立部署在 server-c
│                  │
└──────────────────┘

优点:实现简单、隔离性强、子应用完全独立

缺点:切换应用时整页刷新,用户体验差;应用间状态难以共享

方案三:JS 沙箱 + 动态加载

这是目前最主流的方案(qiankun、Garfish 等),主应用在运行时动态加载子应用的 JS/CSS 资源,并在 JS 沙箱中执行。

主应用运行时
┌────────────────────────────────────────┐
│                                        │
│  1. 监听路由变化                        │
│  2. 匹配子应用                         │
│  3. 加载子应用入口(HTML/JS)           │
│  4. 创建 JS 沙箱                       │
│  5. 在沙箱中执行子应用代码              │
│  6. 将子应用渲染到指定 DOM 容器          │
│                                        │
│  ┌──────────────┐  ┌──────────────┐    │
│  │  JS Sandbox  │  │  JS Sandbox  │    │
│  │  ┌────────┐  │  │  ┌────────┐  │    │
│  │  │ 子应用A │  │  │  │ 子应用B │  │    │
│  │  └────────┘  │  │  └────────┘  │    │
│  └──────────────┘  └──────────────┘    │
│                                        │
└────────────────────────────────────────┘

方案四:Web Components

利用浏览器原生的 Web Components 规范(Custom Elements + Shadow DOM),将子应用封装为自定义元素。

html
<main-app>
  <nav-bar></nav-bar>
  <micro-app-a></micro-app-a>
  <micro-app-b></micro-app-b>
</main-app>

架构模式对比

维度iframe路由分发JS 沙箱Web Components
JS 隔离✅ 天然隔离✅ 独立应用✅ Proxy 沙箱✅ Shadow DOM
CSS 隔离✅ 天然隔离✅ 独立应用⚠️ 需额外处理✅ Shadow DOM
通信成本❌ postMessage❌ 无法直接通信✅ props/事件⚠️ 自定义事件
用户体验❌ 弹窗受限❌ 整页刷新✅ 单页体验✅ 单页体验
接入成本✅ 极低✅ 低⚠️ 中等⚠️ 中等
性能❌ 每次重建上下文❌ 整页重载✅ 增量加载✅ 增量加载
技术栈无关
浏览器兼容⚠️ 需 Proxy⚠️ 需 polyfill

qiankun 深入剖析

核心原理

qiankun 是蚂蚁金服开源的微前端框架,基于 single-spa 进行二次封装。single-spa 解决了子应用的生命周期管理和路由劫持问题,但没有提供 JS 沙箱、样式隔离和 HTML Entry 等能力,qiankun 在此基础上补全了这些核心能力。

qiankun 架构层次:

┌─────────────────────────────────────────┐
│              qiankun                     │
│                                         │
│  ┌───────────┐ ┌──────────┐ ┌────────┐  │
│  │ HTML Entry│ │ JS Sandbox│ │ 样式隔离│  │
│  │ import-   │ │ Proxy /   │ │ Shadow │  │
│  │ html-entry│ │ Snapshot  │ │ DOM /  │  │
│  └───────────┘ └──────────┘ │ Scoped │  │
│                             └────────┘  │
│  ┌──────────────────────────────────┐   │
│  │           single-spa              │   │
│  │  路由劫持 + 生命周期管理            │   │
│  └──────────────────────────────────┘   │
└─────────────────────────────────────────┘

应用生命周期

qiankun 子应用必须导出三个生命周期钩子函数,qiankun 会在合适的时机调用它们:

子应用生命周期流转:

  注册子应用


  ┌───────────┐
  │ bootstrap │  初始化,只在首次加载时调用一次
  └─────┬─────┘


  ┌───────────┐     路由切走      ┌───────────┐
  │   mount   │ ──────────────▶  │  unmount  │
  │  挂载渲染  │                  │  卸载销毁  │
  └─────┬─────┘ ◀──────────────  └─────┬─────┘
        │         路由切回               │
        │                              │
        ▼                              ▼
  ┌───────────┐                  可再次 mount
  │  update   │
  │ (可选)   │
  └───────────┘

子应用入口代码示例:

js
let root = null

export async function bootstrap() {
  console.log('app bootstraped')
}

export async function mount(props) {
  const { container } = props
  root = createApp(App)
  root.mount(container ? container.querySelector('#app') : '#app')
}

export async function unmount() {
  root.unmount()
  root = null
}

主应用注册子应用:

js
import { registerMicroApps, start } from 'qiankun'

registerMicroApps([
  {
    name: 'app-react',
    entry: '//localhost:8001',
    container: '#subapp-container',
    activeRule: '/react',
    props: { token: 'xxx' },
  },
  {
    name: 'app-vue',
    entry: '//localhost:8002',
    container: '#subapp-container',
    activeRule: '/vue',
  },
])

start()

JS 沙箱机制

JS 沙箱是微前端框架的核心能力之一。qiankun 提供了三种沙箱实现,按演进顺序依次为:

沙箱演进:

SnapshotSandbox          LegacyProxySandbox         ProxySandbox
(快照沙箱)               (单实例代理沙箱)           (多实例代理沙箱)
                                                    
浏览器不支持 Proxy        单实例场景                  多实例场景(推荐)
遍历 window 做快照        Proxy 代理 window          每个子应用独立 fakeWindow
性能差,O(n)              修改直接作用于真实 window    完全隔离,互不影响
└─────────────┘          └──────────────────┘       └──────────────────┘
      ↓                         ↓                          ↓
  兼容方案                   过渡方案                    最终方案

SnapshotSandbox(快照沙箱)

原理:在子应用挂载前,遍历 window 上所有属性做一份快照;子应用卸载时,再遍历 window 将变化的属性恢复回去,并把变更记录保存下来。下次挂载时再恢复变更。

激活(mount):
1. 遍历 window,保存快照 snapshot = { ...window }
2. 恢复上次的修改记录 modifyMap

失活(unmount):
1. 遍历 window,diff 出变化 → 存入 modifyMap
2. 从 snapshot 恢复 window

时间线:
━━━━━━━┯━━━━━━━━━━┯━━━━━━━━━━━┯━━━━━━━━━━┯━━━━━
       │ 拍快照    │ 恢复快照   │ 拍快照    │
     mount      unmount      mount     unmount
js
class SnapshotSandbox {
  constructor() {
    this.snapshot = {}
    this.modifyMap = {}
  }

  activate() {
    this.snapshot = {}
    for (const key in window) {
      this.snapshot[key] = window[key]
    }
    Object.keys(this.modifyMap).forEach(key => {
      window[key] = this.modifyMap[key]
    })
  }

  deactivate() {
    this.modifyMap = {}
    for (const key in window) {
      if (window[key] !== this.snapshot[key]) {
        this.modifyMap[key] = window[key]
        window[key] = this.snapshot[key]
      }
    }
  }
}

ProxySandbox(代理沙箱)

这是 qiankun 的最终沙箱方案,每个子应用都有自己独立的 fakeWindow,对全局变量的读写都代理到 fakeWindow 上,实现了真正的多实例隔离。

ProxySandbox 读写流程:

子应用 A 读写 window.xxx          子应用 B 读写 window.xxx
         │                                │
         ▼                                ▼
  ┌──────────────┐                 ┌──────────────┐
  │   Proxy A    │                 │   Proxy B    │
  │              │                 │              │
  │  set(key,val)│                 │  set(key,val)│
  │  → fakeWinA  │                 │  → fakeWinB  │
  │              │                 │              │
  │  get(key)    │                 │  get(key)    │
  │  → fakeWinA  │                 │  → fakeWinB  │
  │  → 找不到则   │                 │  → 找不到则   │
  │    fallback  │                 │    fallback  │
  │    到真实     │                 │    到真实     │
  │    window    │                 │    window    │
  └──────────────┘                 └──────────────┘
         │                                │
         ▼                                ▼
    fakeWindowA                      fakeWindowB
    (独立副本)                        (独立副本)

手写简版 ProxySandbox:

js
class ProxySandbox {
  constructor() {
    const fakeWindow = Object.create(null)
    this.fakeWindow = fakeWindow
    this.active = false

    this.proxy = new Proxy(fakeWindow, {
      get: (target, key) => {
        if (this.active) {
          return key in target ? target[key] : window[key]
        }
        return window[key]
      },

      set: (target, key, value) => {
        if (this.active) {
          target[key] = value
          return true
        }
        return true
      },

      has: (target, key) => {
        return key in target || key in window
      },

      deleteProperty: (target, key) => {
        if (Object.prototype.hasOwnProperty.call(target, key)) {
          delete target[key]
        }
        return true
      },
    })
  }

  activate() {
    this.active = true
  }

  deactivate() {
    this.active = false
  }
}

使用沙箱执行子应用代码的核心原理:

js
function execScriptInSandbox(code, sandbox) {
  const fn = new Function('window', 'self', 'globalThis', `
    with(window) {
      ${code}
    }
  `)
  fn.call(sandbox.proxy, sandbox.proxy, sandbox.proxy, sandbox.proxy)
}

const sandbox = new ProxySandbox()
sandbox.activate()

execScriptInSandbox(`
  window.myVar = 'hello'
  console.log(window.myVar)
`, sandbox)

console.log(sandbox.fakeWindow.myVar)
console.log(window.myVar)

三种沙箱对比

特性SnapshotSandboxLegacyProxySandboxProxySandbox
原理快照 diff单 Proxy 代理 window多 Proxy + fakeWindow
多实例
性能❌ 遍历 window✅ Proxy 拦截✅ Proxy 拦截
隔离性⚠️ 激活/失活切换⚠️ 共享真实 window✅ 完全隔离
兼容性✅ 不需要 Proxy⚠️ 需要 Proxy⚠️ 需要 Proxy

样式隔离

微前端架构中,子应用之间以及子应用与主应用之间的 CSS 冲突是一个重要问题。qiankun 提供了以下几种样式隔离方案:

Shadow DOM 隔离

Shadow DOM 结构:

<div id="subapp-container">
  #shadow-root (open)
  ├── <style> .title { color: red; } </style>
  └── <div id="app">
       └── <h1 class="title">子应用</h1>
      </div>
</div>

外部样式无法穿透 Shadow DOM 边界
Shadow DOM 内部样式也不会泄漏到外部
js
start({
  sandbox: {
    strictStyleIsolation: true,
  },
})

Shadow DOM 的问题:

  • 弹窗(Modal/Dialog)通常挂载到 document.body,脱离了 Shadow DOM,样式丢失
  • 部分第三方库不兼容 Shadow DOM 环境
  • 事件冒泡路径发生变化

Scoped CSS

qiankun 的 Scoped CSS 方案会为子应用的所有样式规则添加特定的属性选择器前缀:

原始样式:
.title { color: red; }
.header .nav { display: flex; }

转换后:
div[data-qiankun="app-react"] .title { color: red; }
div[data-qiankun="app-react"] .header .nav { display: flex; }
js
start({
  sandbox: {
    experimentalStyleIsolation: true,
  },
})

样式隔离方案对比

方案原理优点缺点
Shadow DOM浏览器原生隔离隔离彻底弹窗问题、兼容性
Scoped CSS选择器前缀兼容性好可能存在权重问题
CSS Module编译时类名 hash成熟方案需子应用配合
CSS-in-JS运行时生成样式天然隔离有运行时开销
BEM 命名约定命名空间简单依赖团队规范

应用间通信

props 传递

主应用在注册子应用时通过 props 字段直接传递数据给子应用:

js
registerMicroApps([
  {
    name: 'app-react',
    entry: '//localhost:8001',
    container: '#subapp-container',
    activeRule: '/react',
    props: {
      userInfo: { name: 'admin', role: 'superadmin' },
      utils: { request: axiosInstance },
    },
  },
])

子应用在 mount 生命周期中接收:

js
export async function mount(props) {
  const { userInfo, utils } = props
  store.commit('SET_USER', userInfo)
}

全局状态管理(initGlobalState)

qiankun 提供了一个轻量级的全局状态管理 API:

js
import { initGlobalState } from 'qiankun'

const actions = initGlobalState({
  user: null,
  theme: 'light',
})

actions.onGlobalStateChange((state, prev) => {
  console.log('main app state change:', state, prev)
})

actions.setGlobalState({ user: { name: 'admin' } })

子应用通过 props 接收的 onGlobalStateChangesetGlobalState 参与全局状态通信:

js
export async function mount(props) {
  props.onGlobalStateChange((state, prev) => {
    console.log('sub app state change:', state, prev)
  })

  props.setGlobalState({ theme: 'dark' })
}
全局状态通信流程:

     主应用                    qiankun GlobalState               子应用 A
       │                            │                              │
       │  initGlobalState(state)    │                              │
       │ ─────────────────────────▶ │                              │
       │                            │                              │
       │  setGlobalState(newState)  │                              │
       │ ─────────────────────────▶ │  onGlobalStateChange(cb)     │
       │                            │ ────────────────────────────▶│
       │                            │                              │
       │                            │  setGlobalState(newState)    │
       │  onGlobalStateChange(cb)   │ ◀────────────────────────────│
       │ ◀───────────────────────── │                              │
       │                            │                              │

自定义事件通信

使用自定义事件实现发布-订阅通信:

js
class MicroAppEventBus {
  constructor() {
    this.events = new Map()
  }

  on(event, callback) {
    if (!this.events.has(event)) {
      this.events.set(event, [])
    }
    this.events.get(event).push(callback)
    return () => this.off(event, callback)
  }

  off(event, callback) {
    if (!this.events.has(event)) return
    const cbs = this.events.get(event)
    const index = cbs.indexOf(callback)
    if (index > -1) cbs.splice(index, 1)
  }

  emit(event, ...args) {
    if (!this.events.has(event)) return
    this.events.get(event).forEach(cb => cb(...args))
  }

  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args)
      this.off(event, wrapper)
    }
    this.on(event, wrapper)
  }
}

window.__MICRO_APP_EVENT_BUS__ = new MicroAppEventBus()

Module Federation(模块联邦)

核心原理

Module Federation 是 Webpack 5 引入的一项革命性特性,它允许多个独立构建的应用在运行时共享代码模块。与传统微前端不同,Module Federation 不是以"应用"为粒度,而是以模块为粒度进行共享。

Module Federation 核心概念:

┌────────────────────────────┐      ┌────────────────────────────┐
│        Host(消费者)        │      │       Remote(提供者)       │
│                            │      │                            │
│  import Button from        │      │  exposes:                  │
│    'remote/Button'         │ ───▶ │    './Button': './Button'  │
│                            │      │                            │
│  运行时动态加载远程模块      │      │  构建时生成 remoteEntry.js   │
│                            │      │                            │
└────────────────────────────┘      └────────────────────────────┘
                 │                               │
                 │          Shared               │
                 │     ┌──────────────┐          │
                 └────▶│    react     │◀─────────┘
                       │  react-dom   │
                       │    lodash    │
                       └──────────────┘
                    共享依赖(避免重复加载)

三个核心角色:

Host      ──  消费远程模块的应用
Remote    ──  提供模块给其他应用消费的应用
Shared    ──  多个应用之间共享的公共依赖

一个应用可以同时是 Host 和 Remote,即既消费别人的模块,也暴露自己的模块给别人用。

配置示例

Remote 端配置(提供模块的应用):

js
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'remote_app',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './utils': './src/utils/index',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
}

Host 端配置(消费模块的应用):

js
const { ModuleFederationPlugin } = require('webpack').container

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host_app',
      remotes: {
        remote_app: 'remote_app@http://localhost:3001/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^18.0.0' },
        'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
      },
    }),
  ],
}

Host 端消费远程模块:

js
const RemoteButton = React.lazy(() => import('remote_app/Button'))

function App() {
  return (
    <React.Suspense fallback="loading...">
      <RemoteButton />
    </React.Suspense>
  )
}

运行时容器加载机制

Module Federation 的核心在于运行时远程模块加载。当 Host 需要使用 Remote 模块时,加载流程如下:

Host 加载 Remote 模块的完整流程:

1. Host 启动


2. 遇到 import('remote_app/Button')


3. 加载 remoteEntry.js(Remote 的入口清单)


4. remoteEntry.js 注册 remote_app 容器到全局
   │  window.remote_app = {
   │    get(module) { ... },
   │    init(sharedScope) { ... }
   │  }


5. 初始化共享作用域(Shared Scope)
   │  container.init(__webpack_share_scopes__)
   │  协商 react / react-dom 等共享依赖的版本


6. 获取远程模块
   │  container.get('./Button')
   │  返回一个 factory 函数


7. 执行 factory,得到模块导出


8. 渲染 RemoteButton

Shared 策略详解

Shared 配置决定了多个应用之间如何共享公共依赖,是 Module Federation 中最复杂也最重要的部分:

js
shared: {
  react: {
    singleton: true,
    requiredVersion: '^18.0.0',
    eager: false,
    strictVersion: false,
  },
  lodash: {
    singleton: false,
    requiredVersion: '^4.17.0',
  }
}
配置项说明默认值
singleton是否只允许加载一个版本false
requiredVersion要求的语义化版本范围来自 package.json
eager是否在初始 chunk 中包含false
strictVersion版本不满足时是否抛错false
shareKey共享的 key 名称包名
shareScope共享作用域名称"default"
Shared 版本协商流程:

Host (react@18.2.0)         Remote (react@18.1.0)
         │                           │
         ▼                           ▼
  ┌──────────────────────────────────────┐
  │          Shared Scope                 │
  │                                      │
  │  react:                              │
  │    18.2.0 (from Host)  ◀── 优先使用   │
  │    18.1.0 (from Remote)              │
  │                                      │
  │  singleton: true                     │
  │  → 只保留一个版本(18.2.0)           │
  │                                      │
  │  singleton: false                    │
  │  → 各自使用各自的版本                 │
  └──────────────────────────────────────┘

与 Vite 的结合

Vite 生态通过 @module-federation/vite 插件支持 Module Federation:

js
import { defineConfig } from 'vite'
import { federation } from '@module-federation/vite'

export default defineConfig({
  plugins: [
    federation({
      name: 'remote-app',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button.vue',
      },
      shared: ['vue', 'pinia'],
    }),
  ],
})

Module Federation 2.0 新特性

Module Federation 2.0 由字节跳动团队主导开发,在 1.0 基础上做了大量增强:

Module Federation 2.0 新能力:

┌─────────────────────────────────────────────┐
│                MF 2.0                        │
│                                             │
│  ┌───────────┐  ┌───────────┐  ┌─────────┐  │
│  │ 类型提示   │  │ 运行时插件 │  │ 动态注册 │  │
│  │ TypeScript │  │ Runtime   │  │ Dynamic │  │
│  │ 自动生成   │  │ Plugin    │  │ Remote  │  │
│  │ 类型声明   │  │ System    │  │ 运行时注册│  │
│  └───────────┘  └───────────┘  └─────────┘  │
│                                             │
│  ┌───────────┐  ┌───────────┐  ┌─────────┐  │
│  │ Chrome    │  │ 跨框架支持 │  │ 预加载  │  │
│  │ DevTools  │  │ React/Vue │  │ 支持    │  │
│  │ 调试面板   │  │ /Angular  │  │         │  │
│  └───────────┘  └───────────┘  └─────────┘  │
└─────────────────────────────────────────────┘

关键改进:

  • TypeScript 类型支持:自动生成远程模块的类型声明文件,消费方获得完整的类型提示
  • 运行时插件系统:通过 runtimePlugins 配置,在模块加载的各个阶段注入自定义逻辑
  • 动态 Remote 注册:运行时动态注册远程模块,不需要在构建时确定所有依赖
  • 框架无关:不再绑定 Webpack,支持 Rspack、Vite 等多种构建工具
  • 预加载策略:支持模块预加载,优化首屏性能
js
import { init, loadRemote } from '@module-federation/enhanced/runtime'

init({
  name: 'host_app',
  remotes: [
    {
      name: 'remote_app',
      entry: 'http://localhost:3001/mf-manifest.json',
    },
  ],
  shared: {
    react: {
      version: '18.2.0',
      scope: 'default',
      lib: () => require('react'),
      shareConfig: { singleton: true, requiredVersion: '^18.0.0' },
    },
  },
})

const Button = await loadRemote('remote_app/Button')

主流微前端框架对比

Wujie(无界)

Wujie 是腾讯开源的微前端框架,核心思路是 iframe + WebComponent 的结合体。它利用 iframe 来天然隔离 JS 环境,同时利用 WebComponent(Shadow DOM)来渲染子应用的 DOM,从而兼得隔离性和用户体验。

Wujie 架构原理:

┌─────────────────────────────────────────────┐
│                  主应用                       │
│                                             │
│  ┌─────────────────────────────────────┐    │
│  │         <wujie-app>                 │    │
│  │         (Web Component)             │    │
│  │                                     │    │
│  │  ┌─────────── Shadow DOM ────────┐  │    │
│  │  │                               │  │    │
│  │  │  子应用 DOM 渲染在这里          │  │    │
│  │  │  (样式天然隔离)               │  │    │
│  │  │                               │  │    │
│  │  └───────────────────────────────┘  │    │
│  └─────────────────────────────────────┘    │
│                                             │
│  ┌─────────── 隐藏 iframe ──────────────┐   │
│  │                                      │   │
│  │  子应用 JS 在 iframe 中执行           │   │
│  │  (JS 环境天然隔离)                  │   │
│  │                                      │   │
│  │  通过 Proxy 将 DOM 操作代理到         │   │
│  │  Shadow DOM 中                       │   │
│  │                                      │   │
│  └──────────────────────────────────────┘   │
│                                             │
└─────────────────────────────────────────────┘

核心设计:

  • JS 在 iframe 中执行,天然具备 JS 沙箱能力
  • DOM 渲染在 Shadow DOM 中,天然具备样式隔离
  • 通过 Proxy 代理 document,将 iframe 中的 DOM 操作转发到 Shadow DOM
  • 支持应用保活:iframe 不销毁,切换时只需显示/隐藏
js
import { startApp, bus } from 'wujie'

startApp({
  name: 'sub-app',
  url: 'http://localhost:8001',
  el: '#container',
  exec: true,
  alive: true,
  props: { token: 'xxx' },
})

Micro-app

Micro-app 是京东开源的微前端框架,基于 WebComponent 的轻量化方案。它将子应用封装为自定义元素 <micro-app>,使用方式类似于使用一个 HTML 标签,接入成本极低。

Micro-app 架构:

主应用模板:
┌──────────────────────────────────────┐
│  <div>                               │
│    <micro-app                        │
│      name="sub-app"                  │
│      url="http://localhost:8001"     │
│      baseroute="/sub"                │
│    ></micro-app>                     │
│  </div>                              │
│                                      │
│  ┌──────── <micro-app> 内部 ────────┐ │
│  │                                  │ │
│  │  #shadow-root (open)             │ │
│  │  ├── <micro-app-head>            │ │
│  │  │    └── <style>...</style>     │ │
│  │  └── <micro-app-body>            │ │
│  │       └── <div id="app">...</div>│ │
│  │                                  │ │
│  └──────────────────────────────────┘ │
└──────────────────────────────────────┘
js
import microApp from '@micro-zoe/micro-app'

microApp.start()
html
<micro-app
  name="sub-app"
  url="http://localhost:8001/"
  baseroute="/sub"
></micro-app>

Garfish

Garfish 是字节跳动开源的微前端框架,提供了完整的微前端解决方案,包括子应用加载、JS 沙箱、样式隔离、路由管理等能力。

Garfish 架构:

┌──────────────────────────────────────────────┐
│                 Garfish Core                  │
│                                              │
│  ┌──────────┐ ┌───────────┐ ┌─────────────┐  │
│  │  Loader  │ │  Router   │ │  Lifecycle  │  │
│  │  资源加载 │ │  路由劫持  │ │  生命周期    │  │
│  └──────────┘ └───────────┘ └─────────────┘  │
│                                              │
│  ┌──────────┐ ┌───────────┐ ┌─────────────┐  │
│  │ Sandbox  │ │  Store    │ │   Plugin    │  │
│  │ VM沙箱   │ │  通信机制  │ │   插件系统   │  │
│  └──────────┘ └───────────┘ └─────────────┘  │
│                                              │
└──────────────────────────────────────────────┘

Garfish 的 JS 沙箱采用了基于 VM 模块的沙箱方案,相比 Proxy 沙箱具有更强的隔离能力:

js
import Garfish from 'garfish'

Garfish.run({
  basename: '/',
  domGetter: '#container',
  apps: [
    {
      name: 'sub-app',
      activeWhen: '/sub',
      entry: 'http://localhost:8001',
    },
  ],
  beforeLoad(appInfo) {
    console.log('app before load:', appInfo.name)
  },
})

框架对比表

维度qiankunWujieMicro-appGarfishModule Federation
开源方蚂蚁金服腾讯京东字节跳动Webpack/社区
JS 隔离Proxy 沙箱iframe 沙箱Proxy 沙箱VM 沙箱独立构建
CSS 隔离Shadow DOM / ScopedShadow DOMShadow DOMScoped独立构建
接入方式生命周期导出startApp APIWeb Component 标签生命周期导出模块导入
接入成本中(需改造子应用)低(类组件化)中(需改造子应用)中(需配置构建)
技术栈无关
子应用保活N/A
通信机制props + globalStateprops + eventBusdata 属性 + 事件props + store模块导入
预加载✅ prefetch✅ preload✅ prefetch✅ prefetch
多实例⚠️ 有限
适用场景大型中后台需强隔离场景轻量接入大型应用模块级共享

微前端落地实践

主应用与子应用通信方案选型

通信方案决策树:

是否需要实时双向通信?
├── 是 ──▶ 全局状态管理(initGlobalState / EventBus)
└── 否
    ├── 是否只是初始化参数传递?
    │   └── 是 ──▶ props 传递
    └── 是否跨多个子应用?
        ├── 是 ──▶ 自定义 EventBus / 状态管理库
        └── 否 ──▶ URL 参数 / localStorage

完整的通信方案:

js
class MicroFrontendBridge {
  constructor() {
    this.store = new Map()
    this.listeners = new Map()
  }

  setState(key, value) {
    const oldValue = this.store.get(key)
    this.store.set(key, value)
    this.notify(key, value, oldValue)
  }

  getState(key) {
    return this.store.get(key)
  }

  subscribe(key, callback) {
    if (!this.listeners.has(key)) {
      this.listeners.set(key, new Set())
    }
    this.listeners.get(key).add(callback)
    return () => {
      this.listeners.get(key).delete(callback)
    }
  }

  notify(key, newValue, oldValue) {
    if (!this.listeners.has(key)) return
    this.listeners.get(key).forEach(cb => cb(newValue, oldValue))
  }

  destroy() {
    this.store.clear()
    this.listeners.clear()
  }
}

公共依赖管理

公共依赖管理是微前端中的关键难题。如果每个子应用各自打包 React、Vue 等框架,会导致重复加载、版本冲突、包体膨胀等问题。

依赖管理策略:

方案一:externals + CDN
┌──────────────┐
│  CDN 加载     │ ◀── react / react-dom / vue
│  全局共享     │     通过 <script> 标签加载
└──────┬───────┘

  ┌────▼────┐  ┌────────┐  ┌────────┐
  │ 子应用A  │  │ 子应用B │  │ 子应用C │
  │ external │  │ external│  │ external│
  │ React    │  │ Vue     │  │ React   │
  └─────────┘  └────────┘  └────────┘

方案二:Module Federation Shared
┌──────────────────────────────────┐
│        Shared Scope              │
│  react@18.2.0 (singleton)        │
│  react-dom@18.2.0 (singleton)    │
│  lodash@4.17.21                  │
└──────────────────────────────────┘

方案三:主应用注入
主应用将公共依赖挂载到 window,子应用从 window 读取

externals 配置示例:

js
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
}

路由同步

微前端中路由同步是一个棘手的问题。子应用通常有自己的路由系统(React Router / Vue Router),需要与主应用的路由保持同步。

路由同步机制:

浏览器 URL:  /main/sub-app/detail/123
              │        │
              │        └── 子应用路由:/detail/123
              └── 主应用路由:/main/sub-app

主应用路由变化


┌──────────────────┐
│  路由匹配子应用    │
│  activeRule 判断   │
└────────┬─────────┘

    ┌────▼────┐         ┌─────────────┐
    │  mount  │────────▶│  子应用启动   │
    │  子应用  │         │  读取子路由   │
    └─────────┘         └──────┬──────┘

                        ┌──────▼──────┐
                        │  子应用渲染   │
                        │  对应页面     │
                        └─────────────┘

子应用路由配置(以 Vue Router 为例):

js
const router = createRouter({
  history: createWebHistory(
    window.__POWERED_BY_QIANKUN__ ? '/sub-app' : '/'
  ),
  routes,
})

子应用路由配置(以 React Router 为例):

jsx
function App() {
  const basename = window.__POWERED_BY_QIANKUN__ ? '/sub-app' : '/'

  return (
    <BrowserRouter basename={basename}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/detail/:id" element={<Detail />} />
      </Routes>
    </BrowserRouter>
  )
}

部署策略

部署架构:

┌─────────────────────────────────────────┐
│                  CDN                     │
│                                         │
│  main-app/                              │
│  ├── index.html                         │
│  ├── js/                                │
│  └── css/                               │
│                                         │
│  sub-app-a/                             │
│  ├── index.html                         │
│  ├── js/                                │
│  └── css/                               │
│                                         │
│  sub-app-b/                             │
│  ├── index.html                         │
│  ├── js/                                │
│  └── css/                               │
└──────────────────┬──────────────────────┘

             ┌─────▼─────┐
             │   Nginx    │
             │            │
             │  /  ──────▶  main-app/index.html
             │  /api ────▶  后端服务
             │            │
             └────────────┘

Nginx 配置示例:

nginx
server {
    listen 80;
    server_name app.example.com;

    location / {
        root /usr/share/nginx/html/main-app;
        try_files $uri $uri/ /index.html;
    }

    location /sub-app-a/ {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /sub-app-a/index.html;
        add_header Access-Control-Allow-Origin *;
        add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
        add_header Access-Control-Allow-Headers 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
    }

    location /sub-app-b/ {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /sub-app-b/index.html;
        add_header Access-Control-Allow-Origin *;
    }
}

子应用跨域问题处理——子应用开发服务器需开启 CORS:

js
module.exports = {
  devServer: {
    port: 8001,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
}

性能优化

微前端性能优化策略:

┌──────────────────────────────────────────────┐
│                                              │
│  加载阶段                                     │
│  ├── 子应用资源预加载(prefetch)               │
│  ├── 共享依赖抽取,避免重复加载                  │
│  ├── 子应用入口 HTML 缓存                      │
│  └── 资源 CDN 加速                            │
│                                              │
│  运行阶段                                     │
│  ├── 子应用保活(keep-alive),避免重复渲染       │
│  ├── 沙箱开销优化                              │
│  ├── 懒加载非首屏子应用                        │
│  └── 通信减少序列化开销                        │
│                                              │
│  构建阶段                                     │
│  ├── 子应用独立构建,并行 CI                    │
│  ├── 公共依赖 externals 处理                   │
│  ├── Tree Shaking                            │
│  └── 代码分割(Code Splitting)               │
│                                              │
└──────────────────────────────────────────────┘

预加载策略示例:

js
import { registerMicroApps, start, prefetchApps } from 'qiankun'

registerMicroApps(apps)

start({
  prefetch: 'all',
})

prefetchApps([
  { name: 'app-react', entry: '//localhost:8001' },
  { name: 'app-vue', entry: '//localhost:8002' },
])

基于路由预测的智能预加载:

js
function setupSmartPrefetch(apps) {
  const routeGraph = {
    '/dashboard': ['/analytics', '/settings'],
    '/analytics': ['/dashboard', '/reports'],
    '/settings': ['/dashboard'],
  }

  window.addEventListener('popstate', () => {
    const currentPath = window.location.pathname
    const nextPaths = routeGraph[currentPath] || []

    nextPaths.forEach(path => {
      const targetApp = apps.find(app => path.startsWith(app.activeRule))
      if (targetApp) {
        prefetchApps([targetApp])
      }
    })
  })
}

子应用资源缓存策略:

js
class AppCache {
  constructor() {
    this.cache = new Map()
    this.maxAge = 5 * 60 * 1000
  }

  set(name, resources) {
    this.cache.set(name, {
      resources,
      timestamp: Date.now(),
    })
  }

  get(name) {
    const entry = this.cache.get(name)
    if (!entry) return null
    if (Date.now() - entry.timestamp > this.maxAge) {
      this.cache.delete(name)
      return null
    }
    return entry.resources
  }

  invalidate(name) {
    this.cache.delete(name)
  }

  clear() {
    this.cache.clear()
  }
}

手写简版微前端框架

综合以上知识,实现一个简版微前端框架,涵盖应用注册、生命周期管理、JS 沙箱、路由劫持等核心能力:

js
class MicroFrontend {
  constructor() {
    this.apps = []
    this.currentApp = null
    this.started = false
  }

  registerApps(apps) {
    this.apps = apps.map(app => ({
      ...app,
      status: 'NOT_LOADED',
      sandbox: null,
      bootstrap: null,
      mount: null,
      unmount: null,
    }))
  }

  start() {
    if (this.started) return
    this.started = true
    this.hijackRoute()
    this.reroute()
  }

  hijackRoute() {
    const originalPushState = window.history.pushState
    const originalReplaceState = window.history.replaceState

    window.history.pushState = (...args) => {
      originalPushState.apply(window.history, args)
      this.reroute()
    }

    window.history.replaceState = (...args) => {
      originalReplaceState.apply(window.history, args)
      this.reroute()
    }

    window.addEventListener('popstate', () => {
      this.reroute()
    })
  }

  async reroute() {
    const path = window.location.pathname
    const targetApp = this.apps.find(app =>
      path.startsWith(app.activeRule)
    )

    if (this.currentApp && this.currentApp !== targetApp) {
      await this.unmountApp(this.currentApp)
    }

    if (targetApp) {
      if (targetApp.status === 'NOT_LOADED') {
        await this.loadApp(targetApp)
      }
      await this.mountApp(targetApp)
    }
  }

  async loadApp(app) {
    const html = await fetch(app.entry).then(res => res.text())
    const container = document.querySelector(app.container)

    const { template, scripts } = this.parseHTML(html)
    container.innerHTML = template

    app.sandbox = new ProxySandbox()

    for (const scriptUrl of scripts) {
      const code = await fetch(scriptUrl).then(res => res.text())
      this.execScript(code, app.sandbox)
    }

    const appExports = app.sandbox.proxy.__MICRO_APP_EXPORTS__

    app.bootstrap = appExports.bootstrap
    app.mount = appExports.mount
    app.unmount = appExports.unmount
    app.status = 'LOADED'

    await app.bootstrap()
    app.status = 'BOOTSTRAPPED'
  }

  async mountApp(app) {
    if (app.status === 'MOUNTED') return
    app.sandbox.activate()
    await app.mount({ container: document.querySelector(app.container) })
    app.status = 'MOUNTED'
    this.currentApp = app
  }

  async unmountApp(app) {
    if (app.status !== 'MOUNTED') return
    await app.unmount()
    app.sandbox.deactivate()
    app.status = 'BOOTSTRAPPED'
    this.currentApp = null
  }

  parseHTML(html) {
    const div = document.createElement('div')
    div.innerHTML = html
    const scripts = []

    div.querySelectorAll('script').forEach(script => {
      if (script.src) {
        scripts.push(script.src)
      }
      script.remove()
    })

    return { template: div.innerHTML, scripts }
  }

  execScript(code, sandbox) {
    const fn = new Function('window', 'self', `
      with(window) {
        ;(function() { ${code} }).call(window)
      }
    `)
    fn.call(sandbox.proxy, sandbox.proxy, sandbox.proxy)
  }
}

使用方式:

js
const mf = new MicroFrontend()

mf.registerApps([
  {
    name: 'sub-react',
    entry: 'http://localhost:8001',
    container: '#sub-container',
    activeRule: '/react',
  },
  {
    name: 'sub-vue',
    entry: 'http://localhost:8002',
    container: '#sub-container',
    activeRule: '/vue',
  },
])

mf.start()

面试高频问题

1. 微前端的 JS 沙箱有几种实现方式?各有什么优缺点?

回答思路

从三种沙箱方案展开——快照沙箱、单实例代理沙箱、多实例代理沙箱。重点讲 Proxy 沙箱的实现原理:创建 fakeWindow 作为代理目标,get 时先查 fakeWindow 再 fallback 到真实 window,set 时只写入 fakeWindow。快照沙箱是兼容方案,通过遍历 window 做 diff。还可以提及 Wujie 的 iframe 沙箱方案,它利用浏览器原生的 iframe 隔离能力,无需手动实现沙箱。

2. qiankun 是如何做样式隔离的?Shadow DOM 有什么问题?

回答思路

qiankun 提供两种样式隔离方案:strictStyleIsolation(Shadow DOM)和 experimentalStyleIsolation(Scoped CSS)。Shadow DOM 隔离最彻底,但存在弹窗(Modal)样式丢失问题——因为弹窗通常挂载到 document.body,脱离了 Shadow DOM 的边界。此外,部分第三方库操作 document.querySelector 无法查询到 Shadow DOM 内部元素。Scoped CSS 通过添加属性选择器前缀实现隔离,兼容性更好但可能存在选择器权重问题。

3. Module Federation 和 qiankun 的核心区别是什么?

回答思路

两者解决的问题维度不同。qiankun 是以应用为粒度的微前端方案,关注应用的加载、隔离、通信、生命周期管理;Module Federation 是以模块为粒度的代码共享方案,关注运行时模块的动态加载和依赖共享。qiankun 强调隔离性,Module Federation 强调共享性。在实际项目中,两者可以结合使用——用 Module Federation 共享公共组件库,用 qiankun 管理子应用的加载和隔离。

4. 子应用之间如何通信?有哪些方案?

回答思路

分层展开:

  • props 传递:最直接,主应用通过 props 向子应用传递数据和方法
  • 全局状态:qiankun 的 initGlobalState、Garfish 的 store
  • 自定义事件:EventBus / CustomEvent,发布-订阅模式
  • URL 参数:通过 URL query/hash 传递轻量数据
  • localStorage/sessionStorage:通过存储中转,需注意同步时机
  • BroadcastChannel:浏览器原生跨页面通信 API

推荐组合使用:初始化参数用 props,实时状态同步用全局状态管理,跨应用事件通知用 EventBus。

5. 微前端中的公共依赖应该如何管理?

回答思路

三种主要策略:

  1. externals + CDN:将 React/Vue 等框架通过 CDN 加载并配置 externals,各子应用共享。优点简单直接,缺点是版本必须统一
  2. Module Federation Shared:通过 shared 配置声明共享依赖,运行时自动协商版本。优点灵活,支持版本兼容
  3. 主应用注入:主应用加载公共依赖后挂载到全局,子应用通过 externals 引用

关键原则:核心框架(React/Vue)必须 singleton,工具库(lodash)可以各自打包。

6. 子应用如何做到独立开发、独立部署?

回答思路

独立开发:子应用拥有独立的 Git 仓库、独立的构建配置。通过判断 window.__POWERED_BY_QIANKUN__ 等标识来区分是独立运行还是作为子应用运行,确保子应用既能独立访问也能嵌入主应用。

独立部署:子应用部署到各自的服务器或 CDN 路径下,主应用通过配置 entry URL 来加载。可以使用配置中心动态管理子应用的 entry 地址,做到不修改主应用代码即可上下线子应用。

7. 微前端场景下路由是怎么管理的?

回答思路

主应用通过劫持 history.pushStatehistory.replaceStatepopstate 事件来监听路由变化,匹配 activeRule 决定加载哪个子应用。子应用内部使用自己的路由库(React Router / Vue Router),但需要设置正确的 basename。关键问题:路由切换时需要正确处理子应用的挂载/卸载顺序,避免路由冲突。qiankun 基于 single-spa 实现了这套路由劫持机制。

8. 你会如何从零搭建一个微前端架构?技术选型考虑哪些因素?

回答思路

从业务场景出发:

  1. 需不需要微前端:团队规模是否足够大?是否有技术栈异构需求?是否有遗留系统迁移需求?
  2. 方案选型:纯模块共享选 Module Federation;需要强隔离选 qiankun/Wujie;轻量接入选 Micro-app;字节生态选 Garfish
  3. 架构设计:确定主应用的职责边界(路由管理、权限、布局);子应用的拆分粒度(按业务域);通信方案;公共依赖管理策略
  4. 工程化配套:统一脚手架、统一 CI/CD 流程、统一监控告警、统一日志采集
  5. 渐进式迁移:不要一步到位,先把一个模块拆出来作为试点,验证方案后再逐步推进

延伸阅读

最后更新于:

用心学习,用代码说话 💻