1、乾坤Js隔离机制
最近项目中使用single-spa微前端框架时,会出现不同子应用之间同名变量冲突的问题,这时候就需要使用乾坤框架的Js沙箱隔离机制。乾坤有三种Js隔离机制,并且在源代码中也是以 SnapshotSandbox、LegacySandbox、ProxySandbox三个类名来指代三种不同的隔离机制。下文我们统一以快照沙箱、支持单应用的代理沙箱、支持多应用的代理沙箱,来代表这三种不同的Js隔离机制。下面我们就编码实现这三种沙箱机制的核心逻辑。
2、编码实现三个沙箱的核心逻辑
2.1 支持单应用的SnapShot沙箱
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];
}
}
}
}
// 验证:
let snapshotSandBox = new SnapshotSandBox();
snapshotSandBox.active();
window.city = 'Beijing';
console.log("window.city-01:", window.city);
snapshotSandBox.inactive();
console.log("window.city-02:", window.city);
snapshotSandBox.active();
console.log("window.city-03:", window.city);
snapshotSandBox.inactive();
//输出:
//window.city-01: Beijing
//window.city-02: undefined
//window.city-03: Beijing
快照沙箱主要分为两种状态: 沙箱激活(此时我们的微应用处于运行中,这个阶段有可能对window上的属性进行操作改变)、沙箱失活(此时我们的微应用已经停止了对window的影响);
在沙箱激活的时候:
- 记录window当时的状态(我们把这个状态称之为快照,也就是快照沙箱这个名称的来源);
- 恢复上一次沙箱失活时记录的沙箱运行过程中对window做的状态改变,也就是上一次沙箱激活后对window做了哪些改变,现在也保持一样的改变。
在沙箱失活的时候:
- 记录window上有哪些状态发生了变化(沙箱自激活开始,到失活的这段时间);
- 清除沙箱在激活之后在window上改变的状态,从代码可以看出,就是让window此时的属性状态和刚激活时候的window的属性状态进行对比,不同的属性状态就以快照为准,恢复到未改变之前的状态。
从上面可以看出,快照沙箱存在两个重要的问题:
- 会改变全局window的属性,如果同时运行多个微应用,多个应用同时改写window上的属性,势必会出现状态混乱,这也就是为什么快照沙箱无法支持多个微应用同时运行的原因。关于这个问题,下文中支持多应用的代理沙箱可以很好地解决这个问题;
- 会通过for(prop in window){}的方式来遍历window上的所有属性,window属性众多,这其实是一件很耗费性能的事情。关于这个问题支持单应用的代理沙箱和支持多应用的代理沙箱都可以规避。
2.2 支持单应用的Legacy代理沙箱
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];
}
});
}
}
// 验证:
let legacySandBox = new LegacySandBox();
legacySandBox.active();
legacySandBox.proxyWindow.city = 'Beijing';
console.log('window.city-01:', window.city);
legacySandBox.inactive();
console.log('window.city-02:', window.city);
legacySandBox.active();
console.log('window.city-03:', window.city);
legacySandBox.inactive();
// 输出:
// 输出:
// window.city-01: Beijing
// window.city-02: undefined
// window.city-03: Beijing
从上面的代码可以看出,其实现的功能和快照沙箱是一模一样的,不同的是,通过三个变量来记住沙箱激活后window发生变化过的所有属性,这样在后续的状态还原时候就不再需要遍历window的所有属性来进行对比,提升了程序运行的性能。但是这仍然改变不了这种机制仍然污染了window的状态的事实,因此也就无法承担起同时支持多个微应用运行的任务。
2.3 支持多应用的Proxy代理沙箱
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];
}
});
}
}
// 验证:
let proxySandBox1 = new ProxySandBox();
let proxySandBox2 = new ProxySandBox();
proxySandBox1.active();
proxySandBox2.active();
proxySandBox1.proxyWindow.city = 'Beijing';
proxySandBox2.proxyWindow.city = 'Shanghai';
console.log('active:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);
console.log('active:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);
console.log('window:window.city:', window.city);
proxySandBox1.inactive();
proxySandBox2.inactive();
console.log('inactive:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);
console.log('inactive:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);
console.log('window:window.city:', window.city);
// 输出:
// active:proxySandBox1:window.city: Beijing
// active:proxySandBox2:window.city: Shanghai
// window:window.city: undefined
// inactive:proxySandBox1:window.city: Beijing
// inactive:proxySandBox2:window.city: Shanghai
// window:window.city: undefined
总而言之,Proxy代理沙箱不用遍历window,对性能是一种优化,同时支持多个微应用,是目前最好的微前端沙箱隔离的机制