Appearance
一、qiankun框架是什么?
一、 一句话定义
qiankun(乾坤) 是由蚂蚁金服开源的一款基于 single-spa 的微前端实现框架。 它的核心目标是:帮助企业将巨石应用(Monolith)拆解为多个可以独立开发、独立测试、独立部署的微应用,并在主应用(基座)中无缝拼装,做到“技术栈无关”。
二、 qiankun 到底解决了什么痛点?(核心特性)
在 qiankun 出现之前,团队做微前端要么用原生的 iframe,要么用基础的 single-spa。qiankun 踩在巨人的肩膀上,解决了这两种方案的致命缺陷:
1. 极其简单的接入方式:HTML Entry
- 传统
single-spa的痛点: 采用的是 JS Entry。这意味着子应用不能打包成包含 HTML 的完整项目,只能暴露出一个 JS 入口文件。这不仅严重破坏了子应用原有的 Webpack/Vite 打包配置,而且处理按需加载的 CSS、图片等静态资源路径时极其痛苦。 - qiankun 的解法: 首创了 HTML Entry。主应用只需要配置子应用的 URL(例如
http://localhost:8080),qiankun 内部会通过fetch请求拿到子应用的完整 HTML,然后解析出里面的 JS 和 CSS 标签,动态执行。子应用几乎不需要修改原有的构建配置,像普通的 SPA 应用一样开发即可。
2. 完备的隔离机制(沙箱)
- 原生
single-spa的痛点: 只管子应用的生命周期调度,不管隔离。多个子应用跑在同一个window下,变量和样式必然满天飞。 - qiankun 的解法: 提供了一套开箱即用的沙箱机制。
- JS 隔离: 通过 Proxy 代理
window,实现多实例的变量隔离(我们上一轮详细聊过)。 - CSS 隔离: 提供 Shadow DOM(严格隔离)和 scoped 前缀(实验性隔离)方案。
3. 与无界(Wujie)等新型框架的架构差异
在评估微前端方案时,通常会对比不同的底层隔离路线:
- qiankun 的路线(类 single-spa 演进): 核心是“运行时劫持”。通过重写和代理浏览器的底层 API(如
window、history、addEventListener)来模拟出一个干净的上下文。它的优势是接入自然、DOM 结构扁平;痛点是第三方老旧组件库(如弹窗挂载)容易出现样式丢失或事件失效的兼容性边界问题。 - 新型框架路线(Web Component + iframe): 利用
iframe的物理隔离特性来运行 JS,再结合 Web Components 将 DOM 渲染到主应用。这种路线(也是很多现代微前端方案的选择)能做到极其严苛的物理级隔离,兼容性极高,但通信成本和 DOM 层级复杂度会相应增加。
4. 资源预加载(Prefetch)
qiankun 提供了预加载能力。当用户还在主应用(基座)浏览时,qiankun 会利用浏览器的空闲时间(requestIdleCallback)默默把其他微应用的 JS、CSS 资源下载好。等用户点击切换时,几乎是瞬间呈现,体验极佳。
二、qiankun的js隔离和css隔离原理
一、 JS 隔离原理(JavaScript Sandbox)
qiankun 的 JS 隔离是通过沙箱(Sandbox)机制实现的。为了兼容不同的浏览器环境和多实例场景,qiankun 内部实现了三种不同的沙箱:
1. 快照沙箱(SnapshotSandbox)
这是为了兼容不支持 Proxy 的老旧浏览器(如 IE11)而设计的,只支持单实例(同一时间只能运行一个微应用)。
- 挂载(Mount): 记录当前全局
window的状态(拍个快照)。然后把微应用上次运行修改的变量还原到window上。 - 卸载(Unmount): 将当前
window与挂载前的“快照”进行 Diff 对比,找出微应用运行期间新增或修改的全局变量,保存起来(为了下次挂载时恢复)。最后将window恢复到快照时的干净状态。 - 缺点: 每次都需要遍历整个
window对象,性能极差;且污染了全局window,无法同时运行多个微应用。
2. 单实例代理沙箱(LegacySandbox)
这是在支持 Proxy 的环境下的一种单实例沙箱。
- 原理: 利用
Proxy代理全局window。它内部维护了三个 Map:
addedPropsMap:记录沙箱运行期间新增的全局变量。modifiedPropsOriginalValueMap:记录沙箱运行期间修改的全局变量的原始值。currentUpdatedPropsValueMap:记录沙箱运行期间所有修改和新增的变量的最新值。
- 过程: 微应用修改变量时,会被 Proxy 拦截并记录到这些 Map 中。卸载时,根据记录把
window还原;再次挂载时,把最新值重新应用上。 - 缺点: 虽然性能比快照好,但它本质上**依然在直接修改全局
window**,所以依然只能单实例运行。
3. 多实例代理沙箱(ProxySandbox)—— 🌟 当前主流
这是 qiankun 目前最完美的沙箱方案,支持多个微应用同时运行。
原理: 同样利用
Proxy,但这次**绝对不修改真实的全局window**。过程: qiankun 会为每个微应用创建一个假的
window对象(fakeWindow),并对这个 fakeWindow 进行 Proxy 代理。读(Get): 先在 fakeWindow 里找,找不到再沿着原型链去真实的
window里找。写(Set): 所有的赋值、修改操作,全部被拦截并只作用在 fakeWindow 上。
优势: 真正的
window始终保持干净。多个微应用各自拥有独立的 fakeWindow,互不干扰,完美实现多实例共存。
二、 CSS 隔离原理(Style Isolation)
CSS 隔离主要是为了解决不同微应用之间、微应用与基座之间全局样式冲突的问题。qiankun 提供了两种主要的隔离策略:
1. 严格样式隔离(Strict Style Isolation)
- 配置方式:
strictStyleIsolation: true - 原理: 利用浏览器原生的 Shadow DOM 技术。qiankun 会将微应用的整个 DOM 节点包裹在一个 Shadow Root 中。
- 优势: 物理级别的绝对隔离,里面和外面的样式完全无法互相穿透,0 冲突。
- 致命缺点(痛点): 在使用 React(< 17版本) 或 Ant Design / Element UI 等组件库时,这些库弹出的 Modal(弹窗)、Select(下拉框)默认会挂载到全局的
document.body上。由于它们逃离了 Shadow DOM 的范围,会导致弹窗彻底丢失样式,或者导致 React 16 的事件代理失效(因为事件被 Shadow DOM 阻断了)。
2. 实验性样式隔离(Experimental Style Isolation)
- 配置方式:
experimentalStyleIsolation: true - 原理: 类似于 Vue 的
scoped特性。qiankun 会在微应用挂载时,通过正则遍历其所有的 CSS 规则,为其添加一个特殊的属性选择器前缀。 - 示例: 如果你原本写了
.button { color: red; }, qiankun 在运行时会把它替换成:div[data-qiankun="app-name"] .button { color: red; }。 - 优势: 避免了 Shadow DOM 带来的各种弹窗挂载和事件丢失问题,兼容性更好。
- 缺点: 如果微应用内部动态创建了
<style>标签并插入到head中,或者动态插入了没有被包裹在主容器内的 DOM 节点,样式可能依然会泄露。
(注:在企业实际落地中,为了稳妥,前端团队往往不依赖 qiankun 的 CSS 隔离,而是手动在微应用中使用 CSS Modules、BEM 命名规范,或者配置 Ant Design 的 ConfigProvider 统一修改组件前缀 prefixCls 来实现工程化隔离。)
三、沙箱的相关知识
在前端微服务(尤其是 qiankun)的语境下,“沙箱(Sandbox)”的核心目的只有一个:保护全局 window 对象不被污染。
因为在浏览器中,所有的全局变量、全局函数(如 setTimeout)、全局事件监听,都挂载在 window 上。如果微应用 A 定义了一个 window.name = 'AppA',微应用 B 也跑起来定义了 window.name = 'AppB',两者就会发生灾难性的冲突。沙箱就是为了让它们“以为”自己独占了 window,但实际上互不干扰。
我们可以用两个非常生动的比喻,来深度剖析快照沙箱和代理沙箱的底层逻辑。
1. 快照沙箱 (Snapshot Sandbox) —— “合租房的时光机”
适用场景: 老旧浏览器(不支持 Proxy),只能单实例运行。 核心思想: 记录原貌 -> 随便折腾 -> 走时复原。
生动比喻: 假设全局 window 是一间出租房。
- 挂载(入住): 微应用 A 搬进来了。在它进门前,沙箱先给整个房间拍了一张全景照片(打快照),记录下电视在左边,沙发在右边。
- 运行(生活): 微应用 A 住在里面,把电视砸了,在墙上画画(修改了全局变量,直接污染了真实的
window)。 - 卸载(退租): 微应用 A 要搬走了。沙箱拿出之前拍的照片,把房间一丝不差地恢复成原来的样子(还原快照)。然后把 A 画的画保存起来。
- 再次挂载: 下次 A 再入住,沙箱先把房间恢复成原样,再把 A 上次画的画贴回墙上。
致命缺陷: 因为微应用运行期间,真实的房子(window)确实被弄乱了,所以这个时候绝对不能让微应用 B 搬进来,否则 B 就会看到 A 弄乱的房子。这就解释了为什么快照沙箱绝对无法支持多实例(不能同时运行多个微应用)。同时,每次拍全景照片(遍历整个 window 对象)非常消耗性能。
2. 多实例代理沙箱 (Proxy Sandbox) —— “黑客帝国里的虚拟矩阵”
适用场景: 现代浏览器(支持 ES6 Proxy),支持多实例完美共存。 核心思想: 障眼法,真假美猴王。
生动比喻: 这次,我们利用了 JavaScript 的高级魔法 Proxy(拦截器)。
- 创造幻境: 沙箱不再让微应用直接接触真实的房子(
window)。而是给每个微应用发了一副 VR 眼镜(Fake Window 结合 Proxy)。 - 读操作(Get): 当微应用想看墙上的画(读取
window.document),Proxy 会通过 VR 眼镜,把真实房子的画面透视给它看。 - 写操作(Set): 重点来了!当微应用想在墙上画画(写入
window.abc = 123)时,Proxy 会立刻拦截这个动作。它不会让画笔碰到真实的墙,而是把这个变量记录在 VR 眼镜的缓存里。 - 完美隔离: 微应用 A 戴着眼镜,看到墙上有自己的画,以为自己修改了全局。微应用 B 戴着另一副眼镜,在墙上挂了电视。但摘下眼镜看,真实的房子(真正的
window对象)自始至终一尘不染。
技术实现简述:
javascript
// 极简版的 Proxy 沙箱原理
const fakeWindow = {}; // 虚拟的 window
const proxyWindow = new Proxy(fakeWindow, {
set(target, key, value) {
// 拦截写入:写入操作全部存在 fakeWindow 上,绝对不碰真正的 window
target[key] = value;
return true;
},
get(target, key) {
// 拦截读取:先看 fakeWindow 里有没有,没有再去真正的 window 里找
return target[key] || window[key];
}
});
// 微应用里的代码执行时,其内部的 window 实际上是指向这个 proxyWindow四、addEventListener、setInterval等全局函数,在微应用卸载时,qiankun 的沙箱如何处理这些全局副作用?
一、 如果不处理,会引发什么致命问题?
如果在微应用中执行了 window.addEventListener('resize', cb) 或 setInterval,并且在应用卸载(Unmount)时没有主动销毁它们,会产生典型的全局副作用残留,带来以下三大灾难:
- 严重的内存泄漏(Memory Leak): 虽然微应用的 DOM 节点被销毁了,但
resize事件或setInterval依然在全局window上存活。它们的闭包回调函数(cb)会强引用着微应用内部的变量、React/Vue 组件实例。这导致浏览器的垃圾回收机制(GC)无法释放这部分内存,用户在基座上来回切换几次微应用,浏览器页面就会卡死甚至崩溃。 - 幽灵报错与白屏(Ghost Errors): 当
setInterval定时触发时,它的回调函数可能会去尝试操作已经不存在的 DOM(例如document.getElementById('chart').style.width = ...)。此时由于 DOM 已被卸载,代码会立刻抛出TypeError: Cannot read properties of null的错误。这种报错极其隐蔽,甚至可能直接导致整个基座应用白屏。 - 状态错乱(State Confusion): 如果用户从“微应用 A”切换到“微应用 B”,再切回“微应用 A”。此时
window上会同时存在两个微应用 A 的定时器实例在并行跑,导致业务逻辑完全错乱(比如倒计时越走越快、接口重复轮询等)。
二、 qiankun 是如何处理全局副作用的?
为了防止上述情况,qiankun 在其沙箱系统中实现了“API 劫持(Monkey Patching / Hijacking)”机制。
它的核心原理是:在微应用挂载前,替换掉原生的全局 API;在微应用运行期间“悄悄记账”;在微应用卸载时“集体结账(强制清理)”。
以 setInterval 为例,qiankun 的底层处理逻辑如下(面试时如果能手写这段伪代码,绝对是满分表现):
1. 挂载时(Mount):劫持 API 并记账
qiankun 会保留原生 API 的引用,然后把 window.setInterval 替换成自定义的函数。它内部维护了一个 intervalRegistry(集合)来专门记录当前微应用开启的所有定时器 ID。
javascript
// qiankun 内部的副作用沙箱伪代码
const rawSetInterval = window.setInterval;
const rawClearInterval = window.clearInterval;
// 用来“记账”的集合
const intervalRegistry = new Set();
// 1. 劫持原生的 setInterval
window.setInterval = function (callback, timeout) {
// 调用原生方法,获取真实的 ID
const intervalId = rawSetInterval(callback, timeout);
// 【记账】:把当前生成的 ID 存入沙箱
intervalRegistry.add(intervalId);
return intervalId;
};2. 卸载时(Unmount):强制集体清理
当微应用被卸载时,哪怕开发者在代码里忘了写 clearInterval,qiankun 也会在它的 unmount 生命周期钩子中,遍历 intervalRegistry,替开发者把所有的定时器强行关掉。
javascript
function unmountSandbox() {
// 2. 【集体结账】:遍历记账本,强制清除所有残留的定时器
intervalRegistry.forEach((id) => {
rawClearInterval(id);
});
intervalRegistry.clear(); // 清空记账本
// 3. 恢复原生的 setInterval,不影响其他应用
window.setInterval = rawSetInterval;
}其他副作用的处理
同理,qiankun 也使用相同的“劫持+记账”模式处理了其他常见的全局副作用:
window.addEventListener/window.removeEventListenersetTimeout/clearTimeout- 甚至包括对
document.createElement('script')和<style>的拦截(以防止微应用动态插入的样式污染全局)。
为了让你更直观地理解“裸奔(不清理)”和“qiankun 劫持清理”在应用切换时的区别,我为你生成了一个副作用沙箱模拟器。你可以尝试挂载应用,开启定时器,然后观察卸载后的结果。
五、qiankun 官方推荐的 initGlobalState 通信方案,其底层是如何保证不同微应用之间监听状态改变时互不干扰的?
其实,initGlobalState 的底层并不神秘,它能做到互不干扰且不引发内存泄漏,核心归功于一套带命名空间绑定的发布订阅池。
核心实现原理拆解
qiankun 保证不同微应用监听状态时不串线、不残留,主要通过以下四个步骤实现:
1. 闭包维护的中央状态库 (State Store)
在基座(主应用)调用 initGlobalState(state) 时,qiankun 内部会利用闭包创建一个全局对象,并初始化一个依赖收集池(deps)。这个池子是一个字典(Record),键是微应用的唯一名字(appName),值是对应的回调函数。
2. 高阶函数拦截与命名空间绑定 (Namespace Binding)
这是互不干扰的最关键点! 基座应用拿到的 onGlobalStateChange 是最原始的方法。但在微应用挂载时,qiankun 传递给微应用 props 里的 onGlobalStateChange,是一个被包了一层的高阶函数。
当微应用 A 调用 props.onGlobalStateChange(callback) 时,它内部其实是在执行: deps['微应用A的名字'] = callback; 微应用根本不需要自己传名字,qiankun 在底层已经自动帮它把 appName 作为唯一 Key 绑定了。这样,微应用 A 和微应用 B 的回调在池子里是物理隔离的。
3. 触发与状态比对 (State Diffing)
当任何一个应用(主应用或子应用)调用 setGlobalState(newState) 时,qiankun 会做两件事:
- 浅比较(Shallow Equal): 检查
newState和oldState是否真的发生了变化。如果没变,直接return,防止无效渲染。 - 循环派发: 将新老状态合并后,遍历
deps字典,执行里面所有的回调函数deps[appName](state, prev)。由于每个应用只注册了自己的回调,所以大家都能精准收到通知。
4. 生命周期联动与自动解绑 (Auto Cleanup)
和卸载时的全局副作用清理同理,当微应用 A 被卸载(Unmount)时,qiankun 的生命周期管家会自动执行 delete deps['微应用A的名字']。 即使微应用 A 的开发者忘记手写 offGlobalStateChange,qiankun 也会强制把它的监听器从池子里踢出去,彻底杜绝了“微应用被销毁了,但状态改变时依然触发了它的回调报错”的问题。
源码级伪代码演示
javascript
// qiankun 底层状态管理极简实现
export function initGlobalState(initialState = {}) {
let globalState = { ...initialState };
const deps = {}; // 依赖收集池:{ 'app-a': cb1, 'app-b': cb2 }
// 触发状态改变
function setGlobalState(newState) {
if (newState === globalState) return false;
const prev = globalState;
globalState = { ...globalState, ...newState }; // 合并状态
// 通知所有订阅者
Object.keys(deps).forEach((appName) => {
deps[appName](globalState, prev);
});
return true;
}
// 原始的监听方法 (给基座用)
function onGlobalStateChange(callback) {
deps['master'] = callback; // 基座默认占用 'master' 键
}
// qiankun 内部传给微应用的【特制版】监听方法
// appName 是 qiankun 在装载微应用时自动注入的
function getMicroAppOnGlobalStateChange(appName) {
return function (callback) {
deps[appName] = callback; // 精准隔离!
};
}
// qiankun 在卸载微应用时的自动清理动作
function unmountMicroApp(appName) {
delete deps[appName];
}
return { setGlobalState, onGlobalStateChange };
}