摘要
本文阐述 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,有效地隔离了微应用之间的全局状态,是现代微前端架构中实现多应用支持和环境隔离的关键技术之一。