关于QianKun: qiankun(乾坤)是由蚂蚁金服退出的微前端解决方案,基于Single-Spa进行二次开发,用于实现Web应用由单体应用到多个前端项目聚合的应用。而qiankun在Single-Spa上的封装的核心之一就是实现前端的沙箱隔离机制。
沙箱: 沙箱其实是一种工具,或者可以理解为一个黑盒,用于隔离当前执行的环境作用域和外部的其他作用域。而在JavaScript中就意味着,在沙箱中的操作被限死在当前作用域,不会对其他模块和个人沙箱造成任何影响。
Qiankun的沙箱隔离主要实现了三种模式:
其中LegacySanBox和ProxySanBox都主要是依赖于Proxy API实现(也由此可见Proxy + Reflect的组合在前端的使用中越来越突出了)。 而当在浏览器不支持Proxy的情况下才会降级为使用snapshotSandBox. LegacySandBox和ProxySanBox的不同之处在于:LegacySanBox用于singular单例模式,而多实例的场景将切换为ProxySanBox
export default class SingularProxySanBox implements SanBox {
private addedPropsMapInSandbox = new map<PropertyKey, any>();
private modifiedPropsOriginalValueMapInSandbox = new map<PropertyKey, any>();
private currentUpdatedPropsValueMap = new map<PropertyKey, any>();
name: string;
proxy: WindowProxy;
sandboxRunning = true;
active(){...}
inactive(){...}
constructor(name: string){...}
}
字段 | 解释 |
---|---|
addedPropsMapInSandbox | 用于记录在沙盒运行期间新增的全局变量,用于在卸载自应用时还原全局变量 |
modifiedPropsOriginalValueMapInSandbox | 记录沙盒运行期间更新的全局变量,用于在卸载自应用时还原全局变量 |
currentUpdatedPropsValueMap | 记录在沙盒运行期间操作过的全局变量,用于在激活子应用是还原沙盒的状态 |
name | 沙盒名称 |
proxy | 可以理解为子应用中的Window/Global对象 |
sandboxRunning | 沙盒是否处于运行状态 |
active | 激活沙箱,在子应用挂载时使用 |
inactive | 卸载沙箱,在子应用卸载时使用 |
现在从Window.Proxy的set和get来分析LegacySandBox的沙箱实现:
const rawWindow = window;
const fakeWindow = Object.create(null) as Window;
const proxy = new Proxy(fakeWindow, {
set(_: Window, p: PropertyKey, value: any): boolean {
if(sandboxRunning){
if(!rawWindow.hasOwnProperty(p)){
addedPropsMapInSandbox.set(p, value);
} else if(!modifiedPropsOriginalValueMapInSandbox.has(p) {
const originalValue = (rawWindow as any)[p];
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
}
currentUpdatedPropsValueMap.set(p, value);
(rawWindow as any)[p] = value;
return true;
}
return true;
}
get(_: Window, p: PropertyKey): any {
if(p === "top" || p === "window" || p === "self"){
return proxy;
}
const value = (rawWindow as any)[p];
if(typeof === "function" && !isConstructor(value)){
if(value[boundValueSymbol]){
return value[boundValueSymbol];
}
const boundValue = value.bind(rawWindow);
Object.keys(value).forEach(key => (boundValue[key] = value[key]));
Object.defineProperty(value, boundValueSymbol, { enumerable: false, value: boundValue });
return boundValue;
}
return value;
}
})
当调用set向子应用的proxy/window对象设置属性时,所有的属性设置和更新都会记录在addedPropsMapInSandbox,modifiedPropsOriginalValueMapInSandbox,然后在统一记录到currentUpdatedPropsValueMap中。而调用get从子应用proxy/window中取值时,对于非构造函数的函数取值将会this绑定到window之后在进行返回。
LegacySandBox在激活和卸载子应用时进行沙盒状态的还原和主应用状态的还原过程:
active(){
if(!sandboxRunning){
this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
}
this.sandboxRunning = true;
}
inactive(){
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
this.addedPropsMapInSandbox.forEach((v, p) => setWindowProp(p, undefined, true));
this.sandboxRunning = false;
}
export default class ProxySandbox implements SanBox {
private updateValueMap = new map<PropertyKey, any>();
name: string;
proxy: WindowProxy;
sandboxRunning = true;
active(){...}
inactive(){...}
constructor(name: string){...}
}
字段 | 解释 |
---|---|
updateValueMap | 记录沙箱更新的值,也即是每个子应用中的独立状态值 |
现在从window.Proxy的set和get来分析ProxySandBox如何实现沙箱隔离
constructor(name: string){
this.name = name;
const { proxy, sandboxRunning, updateValueMap } = this;
const boundValueSymbol = Symbol("bound value");
const rawWindow = window;
const fakeWindow = Object.create(null) as Window;
this.props = new Proxy(fakeWindow, {
set(_: Window, p: PerpertyKey, value: any): boolean {
if(sandboxRunning){
updateValueMap.set(p, value);
}
return true;
},
get(_: Window, p: PropertyKey: string): any {
if(p === "top" || p === "window" || p === "self"){
return proxy;
}
const value = updateValueMap.get(p) || (rawWindow as any)[p];
if(typeof === "function" && !isConstructor(value)){
if(value[boundValueSymbol]){
return value[boundValueSymbol];
}
const boundValue = value.bind(rawWindow);
Object.keys(value).forEach(key => (boundValue[key] = value[key]));
Object.defineProperty(value, boundValueSymbol, { enumerable: false, value: boundValue });
return boundValue;
}
return value;
}
})
}
在浏览器不支持Proxy的情况下,将会降级为SnapshotSandbox实现沙箱:
export default class SingularProxySanBox implements SanBox {
name: string;
proxy: WindowProxy;
sandboxRunning = true;
private windowSnapshot!: Window;
private modifyPropsMap: Record<any, any> = {};
active(){...}
inactive(){...}
constructor(name: string){
this.name = name;
this.proxy = window;
this.active();
}
}
字段 | 解释 |
---|---|
windowSnapshot | window状态的快照 |
modifyPropsMap | 沙箱运行期间被修改的属性 |
active(){
if(this.sandboxRunning){
return;
}
this.windowSnapshot = {} as Window;
iter(window, prop => {
this.windowSnapshot[prop] = window[prop];
})
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p];
})
this.sandboxRunning = true;
}
inactive(){
this.modifyPropsMap = {};
iter(window, prop => {
if(window[prop] !== this.windowSnapshot[prop]){
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
})
this.sandboxRunning = false;
}