一、概述
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-SPA | qiankun |
|---|---|---|
| 路由调度 / 生命周期 | ✅ | ✅ 继承 |
| 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 DOM(strictStyleIsolation: true)
将子应用挂载节点包在 Shadow DOM 内,样式天然不穿透边界。
start({ strictStyleIsolation: true })
❌ 缺点:子应用内弹窗(Modal/Drawer)通常挂载到
document.body,会逃出 Shadow DOM 导致样式丢失。
模式二:Scoped CSS(experimentalStyleIsolation: 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-SPA | Module Federation | Web Components | iframe |
|---|---|---|---|---|---|
| 集成时机 | 构建时 | 运行时 | 运行时 | 运行时 | 运行时 |
| JS 隔离 | ❌ | ❌ | ❌ 弱 | ⚠️ 部分 | ✅ 最强 |
| CSS 隔离 | ❌ | ❌ | ❌ | ✅ Shadow DOM | ✅ 最强 |
| 独立部署 | ❌ | ✅ | ✅ | ✅ | ✅ |
| 共享依赖 | ✅ | ⚠️ 手动 | ✅ 原生 | ❌ | ❌ |
| 框架无关 | ❌ | ✅ | ⚠️ | ✅ | ✅ |
| 接入改造成本 | 低 | 高 | 中 | 中 | 极低 |
| 适用场景 | 组件库共享 | 多框架大型平台 | 统一技术栈平台 | 跨框架组件 | 第三方/强隔离 |
八、选型决策树
子应用是否来自第三方 / 有强安全隔离要求?
├── 是 → iframe
└── 否
├── 是否只是共享 UI 组件,不需要独立部署?
│ └── 是 → 构建时集成(npm 包)
└── 否(需要独立部署)
├── 多框架并存(React + Vue + 老系统)?
│ └── 是 → Single-SPA / qiankun
└── 否
├── 技术栈统一 + Webpack 5 体系?
│ └── 是 → Module Federation
└── 追求原生标准 / 跨框架组件复用?
└── 是 → Web Components
九、常见陷阱
- 构建时集成被误当微前端:npm 包方案没有独立部署能力,不是真正的微前端
- CSS 全局污染:未使用沙箱方案时,子应用的
:root变量、全局样式会互相覆盖 - React 多实例问题:Module Federation 未配置
singleton: true时,hooks 会抛出错误 - iframe 弹窗穿透:子应用内的 Modal/Drawer 挂载到
document.body,在 iframe 中无法全屏显示 - postMessage 安全漏洞:接收消息时未校验
event.origin,导致跨站消息注入