功能介绍&模块拆解
在聊原理之前先了解下qiankun提供的能力,一句话介绍qiankun功能:
能根据路由自动调度子应用并实现沙箱(主子、子子应用之间的JS和CSS)隔离。
举个例子:在主应用里注册两个子应用A&B,
1 | import { registerMicroApps } from 'qiankun'; |
在pathname是/baidu_时加载和运行子应用A,如果路由pushState到了/google_时会unmount子应用A再加载和运行子应用B。
从上面的例子可以看出qiankun提供的能力可以划分为3大部分:
- single-spa: 绑定路由的子应用调度,监听路由变化根据当前路由去调度对应的子应用
- import-html-entry: 加载和运行子应用的HTML entry
- sanbox:子应用的隔离,又分为JS的隔离和CSS的隔离

下面分别拆解来分析原理。
Single-spa
一句话介绍single-spa:根据路由变化做子应用调度(子应用生命周期管理)

以单个子应用的生命周期来看流程如下:

注册子应用
1 | singleSpa.registerApplication({ // 注册一个子应用,注册其他子应用同理 |
子应用调度
子应用在自己的入口 js导出了生命周期函数钩子,那在切换路由时子应用A的unmount会执行,子应用B的mount会执行。针对React体系的子应用通常在各生命周期中做如下事情:
1 | /** |
Html-entry-loader
一句话介绍html-entry-loader:把HTML当作子应用的manifest,加载和执行其中的JS拿到JS导出的模块,加载拿到其中的CSS。

特别说明:
- 外链的JS、CSS会被fetch下来以方便执行或提取
- 默认不带沙箱隔离
1 | importHTML('https://xxx.com/subApp.html') |
为啥选择HTML作为子应用manifest的描述载体
主应用需要拿到运行一个子应用所需要的信息,包括:JS、CSS、mountId。
除能用HTML作为载体之外,还有一种方式是通过一个JSON来描述,类似这样:
1 | { |
这两种方式各有千秋:
优点 | 缺点 |
---|---|
完全兼容原来就是以HTML方式输出的网页更灵活,支持HTML中内敛的JS、CSS等复用已有的公知的HTML规范作为协议,而不是新创造一种协议 | 存在信息冗余,传输体积大于JSON解析HTML耗时大于解析JSON |
协议更简单,传输和解析更快 | 操作了一种新协议、规范,需要接入方按此新规范去改造适配,推广成本上升不够灵活 |
选择HTML最主要的原因是 “复用已有的公知的HTML规范作为协议,而不是新创造一种协议”,因为一个子应用可能需要在多个站点投放、或者需要独立运行。
如何提取HTML中的JS、CSS
通过正则表达式提取,源码里枚举了各种可能的情况下的正则提取式:
1 | const ALL_SCRIPT_REGEX = /(<script[\s\S]*?>)[\s\S]*?<\/script>/gi; |
如何执行子应用
1 | function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) { |
with语句
with的初衷是为了避免冗余的对象调用:
1 | foo.bar.baz.x = 1; |
如何执行子应用的JS
1 | const evalCache = {}; |
- 用eval去执行以字符串形式保存的子应用JS
- 借助window.TEMP_EVAL_FUNC记下子应用JS的执行结果,并缓存到evalCache中避免下次重复eval
Sanbox
JS隔离
JS隔离是qiankun最核心最复杂的部分。JS隔离需要实现的目标是:
- 隔离是指对window修改进行隔离,封装污染window
- 避免子应用A污染主应用的window
- 避免子应用A污染子应用B的window
问:沙箱会做避免主应用对子应用A的window污染么?
答:不会,启动一个子应用时,子应用的window继承自主应用
三种JS沙箱实现: SnapshotSandbox、LegacySandbox 、ProxySandbox
不同的JS沙箱实现 | 原理简介 | 优点 | 缺点 | 开启方法 |
---|---|---|---|---|
ProxySandbox | 基于Proxy API实现 | 隔离性和性能较好 | 浏览器兼容性问题,依赖无法polyfill的Proxy API | sanbox = true |
SnapshotSandbox | 基于diff算法实现 | 性能低,只支持单例子应用隔离作用有限 | 浏览器兼容性好,支持IE11 | 用于不支持 Proxy 的低版本浏览器降级使用 |
LegacySandbox | 基于Proxy API实现,现已废弃不推荐使用 | 中间产物 | 中间产物 | singular = true |
- qiankun会优先使用ProxySandbox,对于不兼容Proxy的浏览器会降级到SnapshotSandbox
- ProxySandbox支持同时有多个子应用沙箱运行,SnapshotSandbox无法保证同时有多个子应用时的隔离
- LegacySandbox时历史中间产物,现在已经没有存在的价值,所以废弃不推荐使用
ProxySandbox核心思想
拦截对window
上字段的读&写,每个子应用一个沙箱(一个fakewindow),子应用对window读&写实际是对fakewindow的读写。
- 一个map去存储子应用对window的修改记录,对window的写都会记录在内
- get时优先去map中读,找不到就去外层真实的window上读

如何拦截对window的读写
1 | const fakewindow = new ProxySandbox(); // 给子应用分配的代理window变量 |
子应用代码中对window的读写,实际上变成了对subAppProxy的读写。
SnapshotSandbox核心思想
把主应用的 window 对象做浅拷贝,将 window 的键值对存成一个 Hash Map。之后无论微应用对 window 做任何改动,当要在恢复环境时,把这个 Hash Map 又应用到 window 上就可以了。
微应用 mount 时:
- 先把上一次记录的变更 modifyPropsMap 应用到微应用的全局 window,没有则跳过
- 浅复制主应用的 window key-value 快照 = mainWindowKV,用于下次恢复全局环境
微应用 unmount 时
- 将当前微应用 window 的 key-value = microWindowKV 和 mainWindowKV 进行 Diff,Diff 出来的结果就是 modifyPropsMap
- 将上次快照 mainWindowKV 拷贝到主应用的 window 上,以此恢复环境
JS沙箱逃逸
你的JS里有诸如 document.body.appendChild(scriptElement)
这样的代码,会动态往DOM里面插入JS,如果不处理这些JS会在主应用的 window 上执行可能污染真正的window。
为此,沙箱还会拦截appendChild方法,凡是子应用中appendChild进去的JS都会被fetch下来去沙箱里面执行。
https://github.com/umijs/qiankun/blob/master/src/sandbox/patchers/dynamicAppend/common.ts#L396
CSS隔离
qiankun提供以下三种隔离样式的方式
CSS隔离实现方式 | 原理简介 | 优点 | 缺点 | 开启方法 |
---|---|---|---|---|
CSS生命周期管理 | 子应用之间切换时,是会自动做子应用CSS的加载和卸载的,防止子应用A的CSS代入到子应用B中 | 无额外性能开销兼容性好 | 只能做子应用之间切换时的隔离,无法做主子、并发子的隔离 | 内置逻辑全程开启无法关闭 |
Scopted Style | 给子应用套一层特殊选择器的div修改子应用CSS选择器前缀 | 能做到主子、并发子的隔离 | 提升CSS选择器复杂性,降低页面性能 | experimentalStyleIsolation |
Shadow DOM | 用Shadow DOM包裹 | 能做到主子、并发子的隔离 | 浏览器兼容性问题,依赖无法polyfill的Shadow DOM API子应用需要做一些适配 | strictStyleIsolation |
1. CSS生命周期管理
子应用之间切换时,是会自动做子应用CSS的加载和卸载的,防止子应用A的CSS代入到子应用B中。
2. Scopted Style

3. Shadow DOM
用https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM 包裹子应用DOM区域,防止子应用DOM里面的CSS作用范围跑到子应用之外。
在Github上参与本文讨论