微前端的答案:把一个巨石应用拆成多个独立开发、独立部署、独立技术栈的子应用,由一个基座(主应用)统一加载、路由分发、沙箱隔离。整体体验像一个应用,内部各自自治。
| 维度 | qiankun(乾坤) | micro-app(京东) | wujie 无界(腾讯) | Module Federation(Webpack5) |
|---|---|---|---|---|
| 核心思路 | single-spa 扩展:HTML Entry + Proxy 沙箱 | Web Component 封装子应用 | iframe + Web Component(无 iframe 缺点) | 运行时模块共享(不是应用级) |
| JS 隔离 | Proxy 沙箱 / 快照沙箱 | Proxy 沙箱 + 严格模式 | iframe 天然隔离(真隔离) | 无(共享模块需自己约定) |
| 样式隔离 | scoped 实验性 + 约定 | Shadow DOM 可选 | Web Component + 样式注入 | 无(靠 CSS Modules) |
| 通信 | props / 全局状态(redux) | CustomEvent + 数据通信 | window 代理 + props | 共享模块(双向) |
| 技术栈 | 主流框架皆可 | 主流框架皆可 | 主流框架皆可 | 需统一 Webpack5 |
| 上手成本 | 中(改 entry 为 umd) | 低(侵入小) | 低(侵入最小) | 中(配置复杂) |
| 适用场景 | 中后台系统整合 | 快速接入存量系统 | 强隔离、性能敏感 | 多团队共享组件/库 |
子应用在基座里运行,JS 全局变量(window.foo、挂到 window 的库)会互相污染:子应用 A 设置了全局,子应用 B 读到脏数据,卸载 A 后 B 的依赖也没了。
class SnapshotSandbox { constructor() { this.modifiedProps = {}; // 记录改动 this.active = false; } active() { // 激活时:把 window 现状存快照 this.windowSnapshot = {}; for (const key of Object.keys(window)) { this.windowSnapshot[key] = window[key]; } // 恢复上次退出时的修改 Object.keys(this.modifiedProps).forEach(k => { window[k] = this.modifiedProps[k]; }); } inactive() { // 退出时:记录修改,还原 window 快照 for (const key of Object.keys(window)) { if (window[key] !== this.windowSnapshot[key]) { this.modifiedProps[key] = window[key]; // 存下我改的 window[key] = this.windowSnapshot[key]; // 还原成原来 } } } }
缺点:切换时全量遍历 window 属性,性能差(几毫秒级可接受但不优雅)。
class ProxySandbox { constructor() { const proxy = window; // 最终会替换为 fakeWindow this.fakeWindow = {}; this.active = true; // 拦截所有属性的读写:读不到时查真实 window,写入进 fakeWindow const target = {}; return new Proxy(target, { get(_, key) { // 先从 fake 找,找不到回退真实 window if (key in target) return target[key]; return window[key]; }, set(_, key, value) { target[key] = value; // 子应用的全局变量写进"自己的沙箱" return true; }, }); } } // 子应用代码里 window.a = 1 —— 实际写到沙箱 target,不影响真实 window // 卸载子应用时整个 proxy 丢弃,零残留 —— 这是 Proxy 沙箱完胜快照的原因
window.xxx 会被自动拦截,不需要子应用改代码。body { color: red } 会污染基座和子应用 B// 编译期:给所有选择器加应用前缀(或约定子应用只写带前缀的类名) // .btn {} → .app-a .btn {} // 缺点:漏网之鱼(body/全局样式)仍会泄漏,需配合约定
const host = document.createElement('micro-app-a'); const shadow = host.attachShadow({ mode: 'open' }); shadow.innerHTML = `<style>.btn{color:red}</style><div class="btn">A 应用</div>`; // shadow 内的样式与外部完全隔离,外部选不中里面,里面选不中外面 // 缺点:弹层/全局类库(如 antd modal 挂 body 下)会逃出 shadow,需要挂载点处理
子应用挂载时把它的 <style> 统一收集,卸载时移除,激活时重新注入 —— 避免"脏样式"累积。仍无法阻止运行时互相影响,所以实践上常配 scoped。
| 方案 | 机制 | 适用 |
|---|---|---|
| props 透传 | 基座加载子应用时传入(含方法) | 基座 → 子应用 |
| 全局事件 | CustomEvent / mitt 事件总线 | 松耦合广播 |
| 共享状态库 | 基座提供全局 store(如用 zustand 单例) | 复杂业务状态 |
| URL 路由参数 | 跳转子应用路由带参 | 页面间导航 |
| Module Federation | 共享模块双向引用 | 跨应用复用组件/库 |
公司有三个独立系统:CRM(Vue2)、数据平台(React18)、审批流(老 jQuery)。员工每天切换登录、跳转体验割裂。目标是:统一登录、统一导航、整体像"一个工作台",但不重写任何老系统。
bootstrap/mount/unmount),其余代码零改动/crm/* 挂 CRM,/data/* 挂数据平台,/approve/* 挂审批流// 老系统(Vue2 CRM)改造的最小改动:main.js 加三行 import { createApp } from 'vue'; import App from './App.vue'; let app = null; export async function bootstrap() {} // 生命周期① export async function mount(props) { // 生命周期②:基座调用 app = createApp(App).mount(props.container.querySelector('#app')); } export async function unmount() { // 生命周期③:切走时销毁 app.$destroy(); app = null; }
window.Vue 全局变量(Proxy 沙箱拦截后需在沙箱内提供);③ 弹层组件挂载点需统一配置window.xxx = xxx)不写沙箱。解法:检查子应用 window 写入,全部走沙箱(Proxy 默认拦截)或改用 import。body/ul/div 裸选择器。解法:scoped + 约定所有子应用类名带前缀,或 Shadow DOM。vue-router hash 模式,URL 解析错乱。解法:子应用路由 base 设为自身前缀,用 history 模式。/app1/* 和 /app2/*window.__test 验证)