1、乾坤Js隔离机制三种沙箱的发展史
在前端开发中,为了保证不同应用之间的代码不会相互干扰,我们需要使用Js隔离机制,通常称为沙箱。在乾坤框架中,我们使用三种不同的Js隔离机制,分别是快照沙箱、支持单应用的代理沙箱和支持多应用的代理沙箱。
最初,乾坤框架只有一种沙箱,即快照沙箱,它使用SnapshotSandbox类来实现。但是,快照沙箱有一个缺点,就是需要遍历window上的所有属性,性能较差。随着ES6的普及,我们可以使用Proxy来解决这个问题,于是就诞生了LegacySandbox,它可以实现和快照沙箱一样的功能,但是性能更好。由于LegacySandbox也会污染全局的window,因此它仅允许页面同时运行一个微应用,我们也称之为支持单应用的代理沙箱。
随着业务的发展,我们需要支持一个页面运行多个微应用,于是就有了ProxySandbox,它可以支持多个微应用同时运行。因此,我们称之为支持多应用的代理沙箱。实际上,LegacySandbox在未来可能会被淘汰,因为ProxySandbox可以做到LegacySandbox的所有功能,而快照沙箱由于向下兼容的原因,可能会长期存在。
在编码实现这三种沙箱机制的核心逻辑时,我们需要注意它们之间的差异和适用场景,以便选择合适的沙箱机制来保证应用的安全性和性能。
2、三个沙箱的核心逻辑编码实现
为了更好地理解乾坤框架中的三种Js隔离机制,我们可以使用最基础的语法和最简单的逻辑来实现它们的核心逻辑。虽然这种实现方式可能不够严密,但可以帮助我们更快速地理解其中的原理。
快照沙箱
class SnapshotSandBox {
windowSnapshot = {}
modifyPropsMap = {}
active() {
// 保存window对象上所有属性的状态
for(const prop in window) {
this.windowSnapshot[prop] = window[prop]
}
// 恢复上一次在运行该微应用的时候所修改过的window上的属性
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop]
})
}
inactive() {
for(const prop in window) {
if(window[prop] !== this.windowSnapshot[prop]){
// 记录修改了window上的哪些属性
this.modifyPropsMap[prop] = window[prop]
// 将window上的属性状态还原至微应用运行之前的状态
window[prop] = this.windowSnapshot[prop]
}
}
}
}
window.city = 'Beijing'
console.log('激活之前',window.city)
let snapshotSandBox = new SnapshotSandBox()
snapshotSandBox.active()
window.city = 'Shanghai'
console.log('激活之后',window.city)
snapshotSandBox.inactive()
console.log('失活之后',window.city)
快照沙箱的核心逻辑非常简单,它在激活和失活时各做两件事情。在激活时,它会记录window的状态,也就是快照,以便在失活时恢复到之前的状态。同时,它会恢复上一次失活时记录的沙箱运行过程中对window做的状态改变,保持一致。在失活时,它会记录window上发生了哪些状态变化,并清除沙箱在激活后对window做的状态改变,以便恢复到未改变之前的状态。
然而,快照沙箱存在两个重要的问题。首先,它会改变全局window的属性,如果同时运行多个微应用,多个应用同时改写window上的属性,就会出现状态混乱。这也是为什么快照沙箱无法支持多个微应用同时运行的原因。其次,它会通过for(prop in window){}的方式来遍历window上的所有属性,这是一件非常耗费性能的事情,因为window属性众多。
为了解决这些问题,乾坤框架引入了支持单应用的代理沙箱和支持多应用的代理沙箱。这两种沙箱机制都可以规避快照沙箱的问题。支持单应用的代理沙箱使用Proxy来代理window对象,以便在微应用运行时不会污染全局的window属性。支持多应用的代理沙箱则可以支持多个微应用同时运行,因为它可以为每个微应用创建一个独立的沙箱环境,避免了多个应用之间的状态混乱问题。同时,支持多应用的代理沙箱也不需要遍历window上的所有属性,因为它只会代理微应用需要的属性,从而提高了性能。
支持单应用的代理沙箱
LegacySandbox沙箱主要用到了三个变量:
- addedPropsMapInSandbox:用于记录沙箱激活期间新增的全局变量。
- modifiedPropsOriginalValueMapInSandbox:用于记录沙箱激活期间更新的全局变量。
- currentUpdatedPropsValueMap:持续记录更新的(新增和修改的)全局变量。
class LegacySandBox {
currentUpdatedPropsValueMap = new Map()
modifiedPropsOriginalValueMapInSandbox = new Map();
addedPropsMapInSandbox = new Map();
proxyWindow = {}
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.hasOwnProperty(prop)){
this.modifiedPropsOriginalValueMapInSandbox.set(prop, originalVal)
}
this.currentUpdatedPropsValueMap.set(prop, value)
window[prop] = value
},
get: (target, prop, receiver) => {
return window[prop]
}
})
}
setWindowProp(prop, value, isToDelete = false) {
//有可能是新增的属性,后面不需要了
if(value === undefined && isToDelete) {
delete window[prop]
} else {
window[prop] = value
}
}
active() {
// 恢复上一次微应用处于运行状态时,对window上做的所有修改
this.currentUpdatedPropsValueMap.forEach((value, prop) => {
this.setWindowProp(prop, value)
})
}
inactive() {
// 还原window上原有的属性
this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop) => {
this.setWindowProp(prop, value)
})
// 删除在微应用运行期间,window上新增的属性
this.addedPropsMapInSandbox.forEach((_, prop) => {
this.setWindowProp(prop, undefined, true)
})
}
}
window.city = 'Beijing'
let legacySandBox = new LegacySandBox();
console.log('激活之前',window.city)
legacySandBox.active();
legacySandBox.proxyWindow.city = 'Shanghai';
console.log('激活之后',window.city)
legacySandBox.inactive();
console.log('失活之后',window.city)
上面的代码实现了类似于快照沙箱的功能,即记录window对象的状态,并在沙箱失活时恢复window对象的状态。不同之处在于,LegacySandbox使用了三个变量来记录沙箱激活后window发生变化过的所有属性,避免了遍历window的所有属性来进行对比,提高了程序运行的性能。但是,这种机制仍然会改变window的状态,因此无法承担同时支持多个微应用运行的任务。因此,乾坤框架引入了支持单应用的代理沙箱和支持多应用的代理沙箱,以避免这个问题。支持单应用的代理沙箱使用Proxy来代理window对象,以便在微应用运行时不会污染全局的window属性。支持多应用的代理沙箱则可以为每个微应用创建一个独立的沙箱环境,避免了多个应用之间的状态混乱问题。
支持多应用的代理沙箱
沙箱激活/卸载流程
- 在沙箱激活后,每次获取window属性时,会先从当前沙箱环境的fakeWindow里面查找,如果不存在,就从外部的window里面去查找。这样做可以保证沙箱内部的操作不会影响到全局的window对象。
- 同时,当window对象发生修改时,使用代理的set方法进行拦截,直接操作代理对象fakeWindow,而不是全局的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)=>{
// 设置时只操作fakeWindow
if(this.isRunning){
target[prop] = value;
}
},
get:(target, prop, receiver)=>{
return prop in target ? target[prop] : window[prop];
}
})
}
}
window.city = 'Beijing'
let proxySandBox1 = new ProxySandBox();
let proxySandBox2 = new ProxySandBox();
proxySandBox1.active();
proxySandBox2.active();
proxySandBox1.proxyWindow.city = 'Shanghai';
proxySandBox2.proxyWindow.city = 'Chengdu';
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);
从上面的代码可以看出,ProxySandbox不存在状态恢复的逻辑,因为所有的变化都是沙箱内部的变化,和window没有关系,window上的属性自始至终都没有受到过影响。ProxySandbox支持多个微应用同时运行,也支持单个微应用运行,因此已经成为了乾坤框架的主要沙箱机制。而LegacySandbox则因为历史原因而存在,其在未来的意义不大。而SnapshotSandbox则因为Proxy在低版本浏览器中无法兼容而长期存在。虽然这里的代码逻辑很简单,但是在实际应用中,ProxySandbox需要支持多个微应用运行,因此其内部的逻辑会比SnapshotSandbox和LegacySandbox更加丰富。总之,理解了上述沙箱机制的思路,就可以理解乾坤框架的Js隔离机制。