从阿里QianKun看前端沙箱隔离

关于QianKun: qiankun(乾坤)是由蚂蚁金服退出的微前端解决方案,基于Single-Spa进行二次开发,用于实现Web应用由单体应用到多个前端项目聚合的应用。而qiankun在Single-Spa上的封装的核心之一就是实现前端的沙箱隔离机制。

沙箱: 沙箱其实是一种工具,或者可以理解为一个黑盒,用于隔离当前执行的环境作用域和外部的其他作用域。而在JavaScript中就意味着,在沙箱中的操作被限死在当前作用域,不会对其他模块和个人沙箱造成任何影响。

Qiankun的沙箱隔离主要实现了三种模式:

  • LegacySandbox
  • ProxySandBox
  • snapshotSanBox

其中LegacySanBox和ProxySanBox都主要是依赖于Proxy API实现(也由此可见Proxy + Reflect的组合在前端的使用中越来越突出了)。 而当在浏览器不支持Proxy的情况下才会降级为使用snapshotSandBox. LegacySandBox和ProxySanBox的不同之处在于:LegacySanBox用于singular单例模式,而多实例的场景将切换为ProxySanBox

LegacySanBox

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;
}
  • 在激活沙箱是会通过currentUpdatedPropsValueMap查询子应用的独立状态池(即沙箱激活状态下更新的全局状态),还原子应用状态
  • 卸载沙箱时,通过addedPropsMapInSandbox删除在沙箱运行期间新增的全局状态,通过modifiedPropsOriginalValueMapInSandbox还原沙箱运行期间更新的全局状态

ProxySandbox

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;
		}
	})
}
  • 当调用set向子应用proxy/window中设置值时,所有的属性的设置和更新都会命中updateValueMap, 从而避免对全局状态产生影响,这样就完全隔离了子应用之间的状态,设置值和取值都优先对子应用的updateValueMap进行操作。

SnapshotSandbox

在浏览器不支持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 沙箱运行期间被修改的属性
  • SnapshotSandbox沙箱主要是通过active激活子应用时记录window状态记录,在卸载是通过快照还原window对象实现.
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;
}
  • active函数: 在子应用激活时,为window对象打一个快照,记录沙箱激活前的状态,打完快照之后内部通过modifyPropsMap将window还原到上次沙箱运行环境,也就是恢复沙箱运行期间的window状态
  • inactive函数:沙箱关闭时,通过遍历比较每个属性,将被改变的window对象属性记录在modifyPropsMap中,并通过快照还原被激活前沙箱的状态,相当于时清除沙箱运行期间全局变量的污染。
  • SnapshotSandbox沙箱实现对window状态的隔离管理,但是在子应用运行期间将会对全局window对象进行污染,所以SnapshotSandbox只可以用于在单实例的情况下,在多实例的场景下将不在支持隔离。

你可能感兴趣的:(web,微前端)