Skip to content
页面导航
精简

一、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(如 windowhistoryaddEventListener)来模拟出一个干净的上下文。它的优势是接入自然、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:
  1. addedPropsMap:记录沙箱运行期间新增的全局变量。
  2. modifiedPropsOriginalValueMap:记录沙箱运行期间修改的全局变量的原始值
  3. 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 是一间出租房

  1. 挂载(入住): 微应用 A 搬进来了。在它进门前,沙箱先给整个房间拍了一张全景照片(打快照),记录下电视在左边,沙发在右边。
  2. 运行(生活): 微应用 A 住在里面,把电视砸了,在墙上画画(修改了全局变量,直接污染了真实的 window)。
  3. 卸载(退租): 微应用 A 要搬走了。沙箱拿出之前拍的照片,把房间一丝不差地恢复成原来的样子(还原快照)。然后把 A 画的画保存起来。
  4. 再次挂载: 下次 A 再入住,沙箱先把房间恢复成原样,再把 A 上次画的画贴回墙上。

致命缺陷: 因为微应用运行期间,真实的房子(window)确实被弄乱了,所以这个时候绝对不能让微应用 B 搬进来,否则 B 就会看到 A 弄乱的房子。这就解释了为什么快照沙箱绝对无法支持多实例(不能同时运行多个微应用)。同时,每次拍全景照片(遍历整个 window 对象)非常消耗性能。


2. 多实例代理沙箱 (Proxy Sandbox) —— “黑客帝国里的虚拟矩阵”

适用场景: 现代浏览器(支持 ES6 Proxy),支持多实例完美共存。 核心思想: 障眼法,真假美猴王。

生动比喻: 这次,我们利用了 JavaScript 的高级魔法 Proxy(拦截器)。

  1. 创造幻境: 沙箱不再让微应用直接接触真实的房子(window)。而是给每个微应用发了一副 VR 眼镜(Fake Window 结合 Proxy)
  2. 读操作(Get): 当微应用想看墙上的画(读取 window.document),Proxy 会通过 VR 眼镜,把真实房子的画面透视给它看。
  3. 写操作(Set): 重点来了!当微应用想在墙上画画(写入 window.abc = 123)时,Proxy 会立刻拦截这个动作。它不会让画笔碰到真实的墙,而是把这个变量记录在 VR 眼镜的缓存里
  4. 完美隔离: 微应用 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)时没有主动销毁它们,会产生典型的全局副作用残留,带来以下三大灾难:

  1. 严重的内存泄漏(Memory Leak): 虽然微应用的 DOM 节点被销毁了,但 resize 事件或 setInterval 依然在全局 window 上存活。它们的闭包回调函数(cb)会强引用着微应用内部的变量、React/Vue 组件实例。这导致浏览器的垃圾回收机制(GC)无法释放这部分内存,用户在基座上来回切换几次微应用,浏览器页面就会卡死甚至崩溃。
  2. 幽灵报错与白屏(Ghost Errors):setInterval 定时触发时,它的回调函数可能会去尝试操作已经不存在的 DOM(例如 document.getElementById('chart').style.width = ...)。此时由于 DOM 已被卸载,代码会立刻抛出 TypeError: Cannot read properties of null 的错误。这种报错极其隐蔽,甚至可能直接导致整个基座应用白屏。
  3. 状态错乱(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.removeEventListener
  • setTimeout / 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 会做两件事:

  1. 浅比较(Shallow Equal): 检查 newStateoldState 是否真的发生了变化。如果没变,直接 return,防止无效渲染。
  2. 循环派发: 将新老状态合并后,遍历 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 };
}