摘要
本文阐述 qiankun 微前端框架下的三种 JS 沙箱隔离机制,包括 SnapshotSandbox、LegacySandbox 和 ProxySandbox,探讨各自原理、优缺点与适用场景。
一、JS 沙箱的必要性
为什么需要 JS 沙箱呢?当一个应用(如应用 A)加载时,可能会修改或添加 window 对象的属性。若不加以控制,后续加载的其他应用(如应用 B)就会受影响,导致属性读写冲突。所以,各应用的 js 文件需要独立环境执行,防止 window 全局对象发生属性读写冲突,这个独立执行环境就是 JS 沙箱。
二、qiankun 中的沙箱机制
1. SnapshotSandbox(快照沙箱)
这是 qiankun 最早期的沙箱机制。在每次应用激活和失活时遍历 window 对象的所有属性,记录并恢复其状态。
class SnapshotSandbox {
constructor() {
this.windowSnapShot = {}; // 存储 window 对象的初始快照
this.modifyPropsMap = {}; // 存储全局哪些属性被修改了
}
// 激活
active() {
this.windowSnapShot = {};
// 记录应用 A window 初始状态
Object.keys(window).forEach(prop => {
this.windowSnapShot[prop] = window[prop]
})
// 恢复到应用 A 上次失活之前的状态
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop]
})
}
// 失活
inactive() {
this.modifyPropsMap = {}
Object.keys(window).forEach(prop => {
if (window[prop]!== this.windowSnapShot[prop]) {
this.modifyPropsMap[prop] = window[prop]; // 记录应用 A 所做的所有修改
window[prop] = this.windowSnapShot[prop]; // 将 window 恢复到最初状态
}
})
}
}
它的性能很差,因为需要在每次应用激活和失活时遍历 window 对象的所有属性来记录和恢复其状态,在属性较多或频繁切换应用的情况下,性能瓶颈尤为明显,并且浪费内存。但是优点是可以兼容不支持 Proxy 的旧版浏览器。总的来说,快照沙箱通过拍摄 window 对象的快照来实现状态的隔离,适用于对浏览器兼容性有要求的场景。
2. LegacySandbox(单应用代理沙箱)
这是在 SnapshotSandbox 基础上优化的一种沙箱机制,通过 ES6 的 Proxy 对 window 对象进行更高效的代理。
class LegacySandbox {
constructor() {
this.modifyPropsMap = new Map() // 存储被修改过的属性原始值
this.addedPropsMap = new Map() // 存储新添加的属性值
this.currentPropsMap = new Map() // 存储所有被修改或新添加的属性最新值
// 创建一个 fakeWindow 对象,并使用 Proxy 对其进行代理
const fakeWindow = Object.create(null)
const proxy = new Proxy(fakeWindow, {
get: (target, key, recevier) => {
return window[key]
},
set: (target, key, value) => {
if (!window.hasOwnProperty(key)) {
this.addedPropsMap.set(key, value) // 新添加的属性
} else if (!this.modifyPropsMap.has(key)) {
this.modifyPropsMap.set(key, window[key]) // 被修改的属性原始值
}
this.currentPropsMap.set(key, value) // 所有的添加修改操作都存储一份最新值
window[key] = value
},
})
this.proxy = proxy
}
// 设置 window 对象的属性
setWindowProp(key, value) {
if (value == undefined) {
delete window[key]
} else {
window[key] = value
}
}
// 激活沙箱
active() {
// 恢复到应用 A 上次失活之前的状态
this.currentPropsMap.forEach((value, key) => {
this.setWindowProp(key, value)
})
}
// 失活沙箱
inactive() {
// 被修改的属性重置为原始值
this.modifyPropsMap.forEach((value, key) => {
this.setWindowProp(key, value)
})
// 移除新添加的属性
this.addedPropsMap.forEach((value, key) => {
this.setWindowProp(key, undefined)
})
}
}
性能方面有所优化,通过 Proxy 对 window 对象进行代理,避免了遍历 window 的性能开销。然而,它仍会读写 window 对象,存在全局污染的问题,并且只能支持单个微应用的运行,意味着在一个页面上不能同时运行多个微应用。事实上,LegacySandbox 在未来应该会消失,逐渐被能够同时支持多个微应用的 ProxySandbox 所取代。
3. ProxySandbox(多应用代理沙箱)
这是 qiankun 最先进的一种 JS 沙箱隔离机制,通过 Proxy 对象为每个微应用创建了一个独立的虚拟 window。
class ProxySandbox {
constructor() {
this.running = false
// 使用 Proxy 对 fakeWindow 进行代理
const fakeWindow = Object.create(null)
this.proxy = new Proxy(fakeWindow, {
get: (target, key) => {
return key in target? target[key] : window[key]
},
set: (target, key, value) => {
if (this.running) {
target[key] = value // 将修改操作应用到 fakeWindow 上,而不是真实的 window 对象
}
return true
},
})
}
active() {
if (!this.running) this.running = true
}
inactive() {
this.running = false
}
}
不会操作 window 对象,不存在全局污染的问题,而且在同一页面上也支持多个微应用的同时运行。缺点则是不兼容不支持 proxy 旧版浏览器。通过 Proxy 为每个微应用创建独立的虚拟 window,有效地隔离了微应用之间的全局状态,是现代微前端架构中实现多应用支持和环境隔离的关键技术之一。