一、什么是微前端
微前端借鉴后端微服务理念,将前端单体应用拆分为多个可独立开发、独立部署、独立运行的子应用,再通过主应用统一整合展示。
核心特征:
- 技术栈无关,子应用可自由选择框架(React、Vue、Angular 等)
- 独立开发,各团队在独立仓库互不干扰
- 独立部署,每个子应用单独构建发布
- 运行时集成,子应用动态加载而非构建时打包在一起
- 样式与 JS 隔离,子应用间互不污染
解决的核心问题:
- 巨石应用难以维护:代码量膨胀,构建缓慢,模块耦合严重
- 多团队协作冲突:共同维护一个仓库,代码冲突频繁,发布相互阻塞
- 技术栈升级困难:整个应用绑定同一技术栈,无法局部渐进升级
- 部署耦合:任何小改动都需重新构建部署整个应用,风险高
- 历史系统整合:新旧系统无法共存,完全重写成本极高
二、技术实现方案
iframe
最简单的隔离方案,通过 <iframe> 标签嵌入子应用。浏览器天然提供 JS 和 CSS 的完全隔离,但 UI 体验差(弹窗、滚动、路由同步问题),通信只能依赖 postMessage,且每次加载都是全新页面,性能较差。
<!-- 主应用嵌入子应用 -->
<iframe src="https://sub-app.example.com" style="width:100%;height:100%;border:none;"></iframe>
<script>
// 主应用向子应用发送消息
document.querySelector('iframe').contentWindow.postMessage({ type: 'TOKEN', token: 'xxx' }, '*');
// 子应用接收主应用消息
window.addEventListener('message', (e) => {
if (e.data.type === 'TOKEN') console.log(e.data.token);
});
</script>
Web Components
利用浏览器原生 Custom Elements 将子应用封装为自定义组件,Shadow DOM 提供天然样式隔离。属于浏览器标准能力,无需额外框架,但生态尚不成熟,与主流框架集成有一定成本,IE 兼容性差。
class SubApp extends HTMLElement {
connectedCallback() {
// Shadow DOM 内部样式与外部完全隔离
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>.title { color: red; }</style>
<div class="title">子应用内容</div>
`;
}
disconnectedCallback() {
// 组件卸载时清理资源
}
}
customElements.define('sub-app', SubApp);
<!-- 主应用中像使用普通标签一样嵌入 -->
<sub-app></sub-app>
NPM 包
将子应用打包为 NPM 包由主应用引入,属于构建时集成。优点是简单直接、TypeScript 支持好,缺点是无法独立部署,版本升级需要主应用重新发布,不能做到真正的技术栈无关。
# 子应用发布为 npm 包
npm publish @company/sub-app
# 主应用安装依赖
npm install @company/sub-app
// 主应用直接 import,构建时打包在一起
import SubAppPage from '@company/sub-app';
// 子应用升级后,主应用必须更新依赖版本并重新构建部署
模块联邦(Module Federation)
Webpack 5 内置特性,支持在运行时跨应用共享模块。可以按模块粒度共享,避免重复加载公共依赖。强依赖 Webpack 5,沙箱隔离能力较弱,应用级别的路由管理需要自行实现。
// 子应用 webpack.config.js —— 暴露模块
new ModuleFederationPlugin({
name: 'subApp',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button',
'./utils': './src/utils',
},
shared: ['vue'], // 声明共享依赖,避免重复加载
});
// 主应用 webpack.config.js —— 消费模块
new ModuleFederationPlugin({
name: 'host',
remotes: {
subApp: 'subApp@http://localhost:3001/remoteEntry.js',
},
});
// 主应用中运行时动态加载子应用暴露的模块
const Button = React.lazy(() => import('subApp/Button'));
JS 沙箱
通过代理或快照机制隔离子应用对全局 window 的读写,防止全局变量污染。是当前微前端框架实现 JS 隔离的核心技术手段,具体分为:
- 快照沙箱:激活时对 window 拍快照,卸载时 diff 还原。简单但性能差,不支持多实例并行。
- 代理沙箱(Proxy):基于 ES6 Proxy 拦截 window 操作,每个子应用维护独立的虚拟 window,读写互不干扰,支持多实例并行,是现代浏览器下的推荐方式。
// 快照沙箱:激活时保存快照,卸载时还原
class SnapshotSandbox {
activate() {
this.snapshot = Object.assign({}, window); // 保存当前 window 快照
}
deactivate() {
for (const key in window) {
if (window[key] !== this.snapshot[key]) {
window[key] = this.snapshot[key]; // 还原所有变更
}
}
}
}
// 代理沙箱:每个子应用独立的虚拟 window,互不污染
class ProxySandbox {
constructor() {
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
set(target, prop, value) {
target[prop] = value; // 只写虚拟 window,真实 window 不变
return true;
},
get(target, prop) {
return prop in target ? target[prop] : window[prop]; // 取不到再读真实 window
},
});
}
}
// 多子应用并行效果
// 子应用A: proxy.foo = 'A' → fakeWindowA.foo = 'A'
// 子应用B: proxy.foo = 'B' → fakeWindowB.foo = 'B'
// 真实 window.foo → undefined(始终干净)
2.6 CSS 隔离
防止子应用样式污染主应用或其他子应用,主要有三种方案:
- 动态样式表:子应用激活时注入样式,卸载时移除。实现简单,但多应用并行展示时会冲突。
- Shadow DOM:将子应用挂载在 Shadow DOM 内,浏览器原生保证内外样式完全隔离。隔离彻底,但挂载到
document.body的弹窗、Tooltip 等组件样式会丢失,与主流 UI 库兼容性差。 - Scoped CSS:运行时为子应用每条 CSS 规则动态添加属性选择器前缀(如
div[data-qiankun="app-name"]),实现作用域限定。兼容性好,但动态计算有一定性能开销,无法阻止主应用样式影响子应用。
// 动态样式表:挂载/卸载时插入和移除 <style> 标签
function mountStyles(cssTexts) {
return cssTexts.map(css => {
const el = document.createElement('style');
el.textContent = css;
document.head.appendChild(el);
return el;
});
}
function unmountStyles(styleEls) {
styleEls.forEach(el => el.remove());
}
// Shadow DOM:子应用 DOM 和样式在 shadow-root 内完全隔离
const shadow = container.attachShadow({ mode: 'open' });
shadow.innerHTML = `<style>${subAppStyles}</style>${subAppHTML}`;
// Scoped CSS:运行时动态为每条规则加属性前缀
function scopeCSS(cssText, appName) {
// .btn { color: red } → div[data-qiankun="app"] .btn { color: red }
return cssText.replace(/(^|\})\s*([^{]+)\{/g, (_, prefix, selector) => {
const scoped = selector.split(',')
.map(s => `div[data-qiankun="${appName}"] ${s.trim()}`)
.join(', ');
return `${prefix} ${scoped} {`;
});
}
三、微前端产品方案
qiankun
蚂蚁金服出品,基于 single-spa 封装,是目前社区最成熟的微前端框架。
核心技术:
- HTML Entry:以子应用
index.html为入口,由import-html-entry解析并加载资源,子应用无需改造构建配置 - JS 隔离:现代浏览器默认使用 ProxySandbox(多实例代理沙箱),每个子应用拥有独立虚拟 window;不支持 Proxy 时降级为 SnapshotSandbox
- CSS 隔离:支持 Shadow DOM(
strictStyleIsolation)和 Scoped CSS(experimentalStyleIsolation)两种模式,默认使用动态样式表 - 应用通信:props 单向传递 +
initGlobalState全局状态管理 - 预加载:支持在空闲时预加载子应用资源,加快激活速度
适合场景: 追求稳定大生态、团队规模较大的企业级中后台系统。
无界(Wujie)
腾讯出品,将 iframe 和 Web Components 结合的创新方案。
核心技术:
- JS 隔离:复用 iframe 的 JS 运行环境作为天然沙箱,彻底隔离,无需手动实现代理沙箱
- CSS 隔离:子应用 DOM 渲染在 Web Components 的 Shadow DOM 内,样式天然隔离
- 应用保活:子应用切换时不销毁实例,保留 DOM 和状态,再次进入时秒开
- 预加载:支持子应用预加载和后台静默运行
适合场景: 对隔离性要求极高、需要应用保活(切换不重载)的场景。
MicroApp(京东)
京东出品,基于 Web Components 封装,接入方式最简单。
核心技术:
- JS 隔离:自研 Proxy 沙箱,与 qiankun 类似但实现更轻量
- CSS 隔离:自动为子应用样式添加 CSS 作用域前缀
- 接入方式:以自定义 HTML 标签
<micro-app>的形式嵌入,对主应用几乎零侵入 - Vite 支持:原生支持 Vite 构建的子应用,qiankun 对 Vite 支持较弱
适合场景: 希望以最低成本快速接入微前端、或子应用使用 Vite 构建的项目。
Module Federation(Webpack 5)
Webpack 内置能力,严格来说是模块共享方案而非完整微前端框架。
核心技术:
- 运行时模块共享:应用间可以在运行时互相暴露和消费模块,避免公共依赖重复打包
- 去中心化:每个应用既可以是 Host(消费方)也可以是 Remote(提供方),无需统一主应用
- 无沙箱:没有内置 JS 和 CSS 隔离机制,需要开发者自行约束
适合场景: 深度绑定 Webpack 5、更关注模块级别共享而非应用级别隔离的场景。
产品方案对比
| 方案 | JS 隔离 | CSS 隔离 | 接入成本 | Vite 支持 | 社区生态 |
|---|---|---|---|---|---|
| qiankun | Proxy 沙箱 | Shadow DOM / Scoped | 中 | 较差 | ⭐⭐⭐⭐⭐ |
| 无界 | iframe 天然隔离 | Shadow DOM | 低 | ✅ | ⭐⭐⭐ |
| MicroApp | Proxy 沙箱 | Scoped CSS | 极低 | ✅ | ⭐⭐⭐ |
| Module Federation | ❌ 无 | ❌ 无 | 低 | ✅ | ⭐⭐⭐ |
四、qiankun 核心技术详解
JS 沙箱
SnapshotSandbox(快照沙箱)
激活时遍历 window 保存快照,卸载时对比快照还原所有变更。实现简单,但每次激活/卸载都需遍历整个 window,性能较差,且同一时刻只能运行一个子应用。
class SnapshotSandbox {
activate() {
this.windowSnapshot = {};
for (const key in window) {
this.windowSnapshot[key] = window[key];
}
// 恢复上次该沙箱运行时的修改
Object.keys(this.modifyPropsMap).forEach(key => {
window[key] = this.modifyPropsMap[key];
});
}
deactivate() {
this.modifyPropsMap = {};
for (const key in window) {
if (window[key] !== this.windowSnapshot[key]) {
this.modifyPropsMap[key] = window[key]; // 记录变更
window[key] = this.windowSnapshot[key]; // 还原
}
}
}
}
ProxySandbox(多实例代理沙箱)
每个子应用拥有独立的 fakeWindow,所有对 window 的读写都发生在各自的 fakeWindow 上,真实 window 始终保持干净,天然支持多实例并行。
class ProxySandbox {
constructor() {
const fakeWindow = Object.create(null);
this.proxy = new Proxy(fakeWindow, {
set(target, prop, value) {
target[prop] = value; // 只写入虚拟 window
return true;
},
get(target, prop) {
// 优先读虚拟 window,取不到再读真实 window
return prop in target ? target[prop] : window[prop];
},
has(target, prop) {
return prop in target || prop in window;
}
});
}
}
多实例并行效果:
子应用A: window.foo = 'A' → fakeWindowA.foo = 'A'
子应用B: window.foo = 'B' → fakeWindowB.foo = 'B'
真实 window.foo → undefined(完全干净)
沙箱盲区: JS 沙箱只拦截
window属性读写,对document.body.appendChild、setTimeout、addEventListener 等操作无能为力,子应用卸载时需在unmount钩子中手动清理,否则会内存泄漏。
CSS 隔离
Shadow DOM(strictStyleIsolation)
start({ sandbox: { strictStyleIsolation: true } });
子应用的 DOM 被挂载在 Shadow Root 内,浏览器原生保证边界内外样式互不穿透。隔离最彻底,但挂载到 document.body 的弹窗、下拉菜单等组件会逃出 Shadow DOM 边界导致样式丢失,需要手动将这类组件的挂载节点指定到子应用容器内。
主应用 DOM
└── #micro-container
└── shadow-root ← 样式边界
├── <style>子应用样式</style>
└── <div id="app">子应用内容</div>
Scoped CSS(experimentalStyleIsolation)
start({ sandbox: { experimentalStyleIsolation: true } });
qiankun 拦截子应用的样式注入,在运行时为每条 CSS 规则动态添加属性选择器前缀,将样式的作用域限定在子应用容器内:
/* 原始 */
.btn { color: red; }
/* 处理后 */
div[data-qiankun="vue-app"] .btn { color: red; }
兼容性好,弹窗问题少,是日常更推荐的方案。但动态计算有性能开销,且无法阻止主应用样式向下影响子应用。
CSS 隔离方案对比:
| 方案 | 隔离方向 | 弹窗兼容 | 推荐场景 |
|---|---|---|---|
| 动态样式表(默认) | 子应用间不同时存在 | ✅ | 基础场景 |
| Shadow DOM | 双向完全隔离 | ❌ 需额外处理 | 隔离要求极高 |
| Scoped CSS | 子应用不影响外部 | ✅ | 日常推荐 |
4.3 应用间通信
子应用间的通信分为三种场景:主应用向子应用传递数据、子应用向主应用反馈、子应用之间互相通信。
props 传递
最简单直接的方式,主应用在注册子应用时通过 props 字段传入数据或回调函数。子应用在 mount 钩子中接收。这种方式是单向的,适合传递初始配置、用户信息、或让子应用调用主应用提供的方法(如全局登出)。
// 主应用
registerMicroApps([{
name: 'sub-app',
props: {
token: 'xxx',
userInfo: { name: 'John' },
onLogout: () => { /* 主应用处理登出逻辑 */ }
}
}]);
// 子应用 mount 钩子中接收
export async function mount(props) {
const { token, userInfo, onLogout } = props;
}
initGlobalState(全局状态)
qiankun 内置的发布订阅机制,主应用初始化一个全局状态对象,主应用和所有子应用都可以监听状态变化、也可以更新状态。适合需要跨应用共享且频繁变化的数据,如当前用户信息、主题、语言等。
需要注意的是,子应用只能调用 setGlobalState 修改已存在的一级属性,不能新增顶层字段,状态的结构由主应用初始化时决定。
// 主应用初始化
import { initGlobalState } from 'qiankun';
const actions = initGlobalState({ user: null, theme: 'light' });
actions.onGlobalStateChange((state, prev) => {
console.log('状态变更:', prev, '→', state);
});
// 子应用中(通过 mount props 获取 actions)
export async function mount(props) {
props.onGlobalStateChange((state) => {
console.log('子应用收到状态:', state);
});
props.setGlobalState({ theme: 'dark' }); // 触发所有监听者
}
自定义事件总线
当需要子应用之间直接通信,而不必经过主应用中转时,可以在主应用初始化时挂载一个全局事件总线,所有子应用共享使用。这种方式灵活性最高,但需要注意子应用卸载时要及时 off 事件,避免监听器堆积。
// 主应用初始化,挂载到全局
class EventBus {
constructor() { this.events = {}; }
on(event, fn) { (this.events[event] ??= []).push(fn); }
emit(event, data) { (this.events[event] ?? []).forEach(fn => fn(data)); }
off(event, fn) { this.events[event] = (this.events[event] ?? []).filter(f => f !== fn); }
}
window.__BUS__ = new EventBus();
// 子应用A 发送
window.__BUS__.emit('order:created', { id: 123 });
// 子应用B 接收(unmount 时记得 off)
window.__BUS__.on('order:created', handler);
4.4 生命周期
子应用需要导出三个生命周期钩子供 qiankun 调用:
| 钩子 | 触发时机 | 调用次数 | 常见用途 |
|---|---|---|---|
bootstrap | 资源加载完成后,首次激活前 | 仅一次 | 初始化全局配置 |
mount | 路由匹配,子应用激活 | 多次 | 渲染应用、绑定事件 |
unmount | 路由离开,子应用卸载 | 多次 | 销毁实例、清理定时器和事件监听 |
首次进入:bootstrap → mount
路由切换:unmount → mount(重复)
五、微前端优势
- 技术栈自由:各子应用独立选型,新技术可在单个子应用中试用,不影响整体
- 独立部署:子应用单独构建发布,发布频率和节奏互不干扰
- 团队自治:团队边界与应用边界对齐,减少跨团队协作摩擦
- 渐进式迁移:可将旧系统逐模块替换为新技术栈,无需一次性重写
- 故障隔离:单个子应用崩溃不影响主应用和其他子应用
- 按需加载:用户只加载当前访问模块,首屏资源体积更小