主题
微前端
什么是微前端
微前端(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 unmountjs
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)三种沙箱对比
| 特性 | SnapshotSandbox | LegacyProxySandbox | ProxySandbox |
|---|---|---|---|
| 原理 | 快照 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 接收的 onGlobalStateChange 和 setGlobalState 参与全局状态通信:
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. 渲染 RemoteButtonShared 策略详解
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)
},
})框架对比表
| 维度 | qiankun | Wujie | Micro-app | Garfish | Module Federation |
|---|---|---|---|---|---|
| 开源方 | 蚂蚁金服 | 腾讯 | 京东 | 字节跳动 | Webpack/社区 |
| JS 隔离 | Proxy 沙箱 | iframe 沙箱 | Proxy 沙箱 | VM 沙箱 | 独立构建 |
| CSS 隔离 | Shadow DOM / Scoped | Shadow DOM | Shadow DOM | Scoped | 独立构建 |
| 接入方式 | 生命周期导出 | startApp API | Web Component 标签 | 生命周期导出 | 模块导入 |
| 接入成本 | 中(需改造子应用) | 低 | 低(类组件化) | 中(需改造子应用) | 中(需配置构建) |
| 技术栈无关 | ✅ | ✅ | ✅ | ✅ | ✅ |
| 子应用保活 | ❌ | ✅ | ✅ | ❌ | N/A |
| 通信机制 | props + globalState | props + eventBus | data 属性 + 事件 | 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. 微前端中的公共依赖应该如何管理?
回答思路:
三种主要策略:
- externals + CDN:将 React/Vue 等框架通过 CDN 加载并配置 externals,各子应用共享。优点简单直接,缺点是版本必须统一
- Module Federation Shared:通过 shared 配置声明共享依赖,运行时自动协商版本。优点灵活,支持版本兼容
- 主应用注入:主应用加载公共依赖后挂载到全局,子应用通过 externals 引用
关键原则:核心框架(React/Vue)必须 singleton,工具库(lodash)可以各自打包。
6. 子应用如何做到独立开发、独立部署?
回答思路:
独立开发:子应用拥有独立的 Git 仓库、独立的构建配置。通过判断 window.__POWERED_BY_QIANKUN__ 等标识来区分是独立运行还是作为子应用运行,确保子应用既能独立访问也能嵌入主应用。
独立部署:子应用部署到各自的服务器或 CDN 路径下,主应用通过配置 entry URL 来加载。可以使用配置中心动态管理子应用的 entry 地址,做到不修改主应用代码即可上下线子应用。
7. 微前端场景下路由是怎么管理的?
回答思路:
主应用通过劫持 history.pushState、history.replaceState 和 popstate 事件来监听路由变化,匹配 activeRule 决定加载哪个子应用。子应用内部使用自己的路由库(React Router / Vue Router),但需要设置正确的 basename。关键问题:路由切换时需要正确处理子应用的挂载/卸载顺序,避免路由冲突。qiankun 基于 single-spa 实现了这套路由劫持机制。
8. 你会如何从零搭建一个微前端架构?技术选型考虑哪些因素?
回答思路:
从业务场景出发:
- 需不需要微前端:团队规模是否足够大?是否有技术栈异构需求?是否有遗留系统迁移需求?
- 方案选型:纯模块共享选 Module Federation;需要强隔离选 qiankun/Wujie;轻量接入选 Micro-app;字节生态选 Garfish
- 架构设计:确定主应用的职责边界(路由管理、权限、布局);子应用的拆分粒度(按业务域);通信方案;公共依赖管理策略
- 工程化配套:统一脚手架、统一 CI/CD 流程、统一监控告警、统一日志采集
- 渐进式迁移:不要一步到位,先把一个模块拆出来作为试点,验证方案后再逐步推进