上篇中咱们分析了css
沙箱,看起来并没有很重要,但是相比起来js、window
沙箱的重要性就大了很多。qiankun
中默认是开启 的,说一下真实遇到的案例:
我之前搞一个JSP
项目就发现在那个工程里面JSON.stringify({name: '张三'})
,得到'"{\\"name\\":\\"张三\\"}"'
,看着好像并没有什么问题,但是我JSON.parse()
解析时发现得到的还是字符串 ,经过审查才发现里面的JSON
方法被重写 了!!!
其实这只是我遇到的一个案例,有些插件为了满足功能把好多方法都会重写,比如Vue2
为了监听重写了Array
的一系列方法、single-spa
为了监听路由重写了pushState、replaceState
等等(这种是不影响原API使用 的)。就怕有些像我上面那个案例样子直接影响了原API 的执行结果。
可以看出 js、window
很有必要,从源码中看到qiankun
共实现了3种沙箱:
- 针对不支持
proxy api
使用的是快照的形式实现的。SnapshotSandbox
- 针对支持
proxy api
使用的是proxy
的形式实现的。ProxySandbox
咱们在上一篇中有说qiankun
的主应用和子应用是公用了一个HTML
,那么这里你可以想下?他俩是不是公用一个window
,当然是肯定的啦!
公用一个window,那么切换子应用的时候要把上一个应用放在window
的方法给清除掉,不然可能就会给下个应用照成影响了。
就例如: 我们想在一个A应用 需要监听console
的日志,那么咱们重写下console
的API,做数据上报 。但是我切换到了B应用 没有这个需求!
那么咱们需要再A应用离开 的时候,记录下他更改了哪些东西,因为咱们再回到A应用 时还要有这些方法。并且把他对console.log
给还原回来
这也就是SnapshotSandbox
快照沙箱 的实现。
SnapshotSandbox
沙箱
上面咱们其实已经把快照沙箱说的很清楚了,那么这里咱们看看怎么才能把window
里面的方法记录下来呢?并且还原的呢?
js
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';
function iter(obj: typeof window, callbackFn: (prop: any) => void) { // 循环windows
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const prop in obj) {
// patch for clearInterval for compatible reason, see #1490
if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {
callbackFn(prop);
}
}
}
/**
* 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
*/
export default class SnapshotSandbox implements SandBox {
proxy: WindowProxy;
name: string;
type: SandBoxType;
sandboxRunning = true;
private windowSnapshot!: Window;
private modifyPropsMap: Record<any, any> = {};
constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
}
active() { // 初次进入该微应用时 比如这个时候还未更改过window 咱们以空对象的视角去看 {}
// 记录当前快照
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});
// 恢复之前的变更
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p]; // 5. 再次载入微应用时,恢复API
});
this.sandboxRunning = true;
// 1、active执行完就会接着执行 子应用
// 2. 子应用要对window.console.log做重写
}
inactive() { // 离开微应用时
this.modifyPropsMap = {};
iter(window, (prop) => { // 3. 离开时开始跟快照做 对比 记录哪里不一样,并且把原来更改的给回退回来
if (window[prop] !== this.windowSnapshot[prop]) {
// 记录变更,恢复环境
this.modifyPropsMap[prop] = window[prop]; // 记录更改 为第5步做准备
window[prop] = this.windowSnapshot[prop]; // 4. 把变动更改回来
}
});
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
}
this.sandboxRunning = false;
}
}
在上面代码中有注释,咱们这里再重复一遍,避免大家看不懂。
-
- 执行
active
时,也就时再微应用首次加载时,把window
的快照windowSnapshot
浅拷贝记录下来。
- 执行
-
- 经过执行js,假如这个时候对
window.console
进行了重写。
- 经过执行js,假如这个时候对
-
- 要离开子应用时执行
inactive
,这个时候遍历当前的window
对象跟快照windowSnapshot
做对比。
- 要离开子应用时执行
-
- 找到不一样的地方,把原来改过的给恢复回来。并且记录都做了哪些更改
modifyPropsMap
.
- 找到不一样的地方,把原来改过的给恢复回来。并且记录都做了哪些更改
-
- 再次进入子应用时,把
modifyPropsMap
里面的内容给恢复到window上。
- 再次进入子应用时,把
我在给大家在看个例子如下:

经测试快照只是做了一层对比(本身就是浅拷贝),这么更改并不会被记录下来。会影响其他子应用的,如有有类似的需求需要自己记录下原本的变更。
ProxySandbox
沙箱
ProxySandbox
是针对支持proxy
的浏览器实现的,那么它又是如何实现的呢?
其实如果步考虑各种兼容 来看会比较好理解一点,我们先看下简易版本的ProxySandbox
的实现:
其实是执行scripts
时,把代理的proxy
作为参数穿了进来,可以看下如下几个打印:
js
const proxy = {}
(function(window) {
// code 部分
console.log(window,window.console, console) // {}, undefined, console {debug: ƒ, error: ƒ, info: ƒ, log: ƒ, warn: ƒ, ...}
}(proxy))
那么如何兼容console
的API
呢?使用with
,先看一个使用with
的示例
js
const person = {
name: "Alice",
age: 25,
greet: function() {
console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
}
};
with (person) {
console.log(name); // 输出 Alice
console.log(age); // 输出 25
greet(); // 输出 Hello, my name is Alice and I am 25 years old.
}
with
语句用于扩展一个语句的作用域链。它允许你在一个代码块中使用一个对象的属性和方法,而不需要重复地引用该对象。不过,由于其可能导致代码难以理解和维护,with
语句在严格模式下是被禁止使用的。
在回到咱们的执行scripts
中使用with
是怎么做的呢?如下:
js
const proxy = new Proxy(window, {})
(function(window) {
with (proxy) {
// code 部分
console.log(window,window.console, console) // {}, undefined, console {debug: ƒ, error: ƒ, info: ƒ, log: ƒ, warn: ƒ, ...}
}
}(proxy))
这样就解决了使用window
全局API
,并且走代理 的问题了。
如上那种代理其实也会污染原本的windows
的(被浅拷贝了),可以看个代理
的示例:

那么乾坤中的ProxySandbox
是如何实现的呢?可以一起看下createFakeWindow
API
ts
const rawObjectDefineProperty = Object.defineProperty;
function createFakeWindow(globalContext: Window) {
const propertiesWithGetter = new Map<PropertyKey, boolean>();
const fakeWindow = {} as FakeWindow;
Object.getOwnPropertyNames(globalContext)
.filter((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
return !descriptor?.configurable;
}) //1. 只剩下这些全局属性: ['Infinity', 'NaN', 'undefined', 'window', 'document', 'location', 'top', 'chrome']
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p); // 2. 获取到全局属性的描述符
if (descriptor) {
const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
if (
p === 'top' ||
p === 'parent' ||
p === 'self' ||
p === 'window' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
descriptor.configurable = true;
if (!hasGetter) {
descriptor.writable = true;
}
}
if (hasGetter) propertiesWithGetter.set(p, true);
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor)); // 3. 创建一个不可配置的属性,并冻结它,注入到fakeWindow
}
});
return {
fakeWindow,
propertiesWithGetter,
};
}
可以看到代理成功并且隔离环境了:

这里看到其实咱们没有更改原本windows全局的API
,只是新增了一些属性。如果我在乾坤一个子应用环境 下更改了window
原本全局的属性,那么在子应用中,这个属性也会被更改。
下图是一个乾坤应用环境的示例:可以看到我在app-vue-hash
应用下更改了console.log
,然后在app-vue-history
应用下也被更改了。


其实可以在源码proxy
的get
上看到,如果fakeWindow
没有该API,还是会从全局的window
上获取的。
关于为什么没有深拷贝window
上的APi 做绝对隔离,这里的话应该是出于性能方面考虑吧!
总结
到这里其实已经完成所有qiankun
的分享,各个方面都详细说了它的优缺点。
因为他是容器、子应用的模式,可以把全局的方法放在容器的windows
上,比如
- 可以放封装好的
axios
,然后达到统一后端的API交互,就不用每个模块都引入axios
的相关代码了。 - 可以放一个
发布订阅者
的API,达到跨应用交互api调用的效果。 - 如果框架都一致,可以考虑把组件都放在容器的
windows
上,然后子应用就可以直接用了。
Q:import-html-entry
中的子应用信息缓存,会照成内纯泄漏吗?
js
const styleCache = {};
const scriptCache = {};
const embedHTMLCache = {};
A:正常应该是不会的,因为他不是持续增长的,他是随着你加载过的子应用数量增长的。除非在特殊场景,比如你的子应用成百上千 ,并且还都在一个tab页面都执行过了。无特殊情况下可以放心。
Q: 如果我们研发团队代码都很标准 ,那么是不是可以不用开启沙箱了?
A: 可以的,其实正常项目也很少在全局window
上放东西。只要不在window
放全局的方法以及注意body、html、:root
那么久可以不开启。