使用微前端方案时,我们要确保应用之间全局变量不能互相影响。比如,子应用 A 给 window 对象定义了新的属性 - globalState,子应用 B 也给 window 对象定义了新的属性 globalState,那我们就需要确保子应用 A 和子应用 B 能获取到各自定义的 globalState。最容易想到的方法就是通过人为约定添加命名前缀的方式来进行 js 隔离,如子应用 A 给 window 对象定义了新的属性 - A_globalState,子应用 B 给 window 对象定义了新的属性 B_globalState。这种方式低效不说,还容易引发 bug。
这个时候, 我们可以引入沙盒, 即sandbox, 这个计算机世界的常见概念, 来帮助我们解决这个问题。沙盒是计算机安全领域中的一种安全机制,可以为运行中的程序提供隔离的环境. 运用到微前端的场景, 我们可以尝试通过沙盒为每个子应用提供隔离的运行环境,保证子应用的 js 代码在执行时使用的全局变量都是独属于当前子应用的。
本文总结归纳了几种常见的JavaScript沙箱实现原理和思路
快照沙箱
class SnapshotSandBox{
windowSnapshot = {};
modifyPropsMap = {};
active(){
for(const prop in window){
this.windowSnapshot[prop] = window[prop];
}
Object.keys(this.modifyPropsMap).forEach(prop=>{
window[prop] = this.modifyPropsMap[prop];
});
}
inactive(){
for(const prop in window){
if(window[prop] !== this.windowSnapshot[prop]){
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}
}
}
顾名思义, 就是给window对象打一个快照, 这样就可以随时对window对象进行还原. 主要步骤如下:
在沙箱激活的时候:
- 记录window当时的状态
-
恢复上一次沙箱失活时记录的沙箱运行过程中对window做的状态改变,也就是上一次沙箱激活后对window做了哪些改变,现在也保持一样的改变。
在沙箱失活的时候
- 记录window上有哪些状态发生了变化
- 清除沙箱在激活之后在window上改变的状态
快照沙箱存在两个重要的问题:
- 会改变全局window的属性,如果同时运行多个微应用,多个应用同时改写window上的属性,势必会出现状态混乱,这也就是为什么快照沙箱无法支持多各微应用同时运行的原因
- 会通过
for(prop in window){}
的方式来遍历window上的所有属性,window属性众多,这其实是一件很耗费性能的事情
proxy代理沙箱
class LegacySandBox{
addedPropsMapInSandbox = new Map();
modifiedPropsOriginalValueMapInSandbox = new Map();
currentUpdatedPropsValueMap = new Map();
proxyWindow;
setWindowProp(prop, value, toDelete = false){
if(value === undefined && toDelete){
delete window[prop];
}else{
window[prop] = value;
}
}
active(){
this.currentUpdatedPropsValueMap.forEach((value, prop)=>this.setWindowProp(prop, value));
}
inactive(){
this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop)=>this.setWindowProp(prop, value));
this.addedPropsMapInSandbox.forEach((_, prop)=>this.setWindowProp(prop, undefined, true));
}
constructor(){
const fakeWindow = Object.create(null);
this.proxyWindow = new Proxy(fakeWindow,{
set:(target, prop, value, receiver)=>{
const originalVal = window[prop];
if(!window.hasOwnProperty(prop)){
this.addedPropsMapInSandbox.set(prop, value);
}else if(!this.modifiedPropsOriginalValueMapInSandbox.has(prop)){
this.modifiedPropsOriginalValueMapInSandbox.set(prop, originalVal);
}
this.currentUpdatedPropsValueMap.set(prop, value);
window[prop] = value;
},
get:(target, prop, receiver)=>{
return target[prop];
}
});
}
}
从上面的代码可以看出,其实现的功能和快照沙箱是一模一样的,不同的是,通过三个变量来记住沙箱激活后window发生变化过的所有属性,这样在后续的状态还原时候就不再需要遍历window的所有属性来进行对比,提升了程序运行的性能。但是这仍然改变不了这种机制仍然污染了window的状态的事实,因此也就无法承担起同时支持多个微应用运行的任务。
支持多应用的代理沙箱
class ProxySandBox{
proxyWindow;
isRunning = false;
active(){
this.isRunning = true;
}
inactive(){
this.isRunning = false;
}
constructor(){
const fakeWindow = Object.create(null);
this.proxyWindow = new Proxy(fakeWindow,{
set:(target, prop, value, receiver)=>{
if(this.isRunning){
target[prop] = value;
}
},
get:(target, prop, receiver)=>{
return prop in target ? target[prop] : window[prop];
}
});
}
}
从上面的代码可以发现,ProxySandbox,完全不存在状态恢复的逻辑,同时也不需要记录属性值的变化,因为所有的变化都是沙箱内部的变化,和window没有关系,window上的属性至始至终都没有受到过影响。
以上总结了三个js沙盒隔离机制, 并给出了极简代码实现, 相信读者朋友如果理解了上面的思路,就可以说已经理解了乾坤的Js隔离机制。