微前端沙箱隔离机制深度解析:Proxy沙箱与快照沙箱实现原理
在微前端架构中,“JS 沙箱(JavaScript Sandbox)”是确保多个独立子应用能够在同一个浏览器标签页中安全共存、互不干扰的核心底座。
如果缺乏沙箱机制:
- 子应用 A 执行了
window.title = '周报看板'或window.userInfo = { role: 'admin' }; - 当路由切换到子应用 B 时,子应用 B 可能会读取到 A 遗留的脏全局变量,甚至把主应用的全局方法意外覆盖,引发难以复现的内存泄漏与逻辑崩溃。
为了实现全局变量的“动态隔离与退出自动复原”,微前端框架(如 qiankun / single-spa)实现了两大核心沙箱机制:快照沙箱(Snapshot Sandbox)与现代 Proxy 沙箱(Proxy Sandbox)。
本文深入剖析两种沙箱的底层运行机制与手写实现原理。
方案一:快照沙箱(面向低版本不支持 Proxy 浏览器的降级方案)
快照沙箱的运行哲学非常质朴:在子应用挂载(Mount)和卸载(Unmount)时,对window对象的全量属性进行深浅拷贝遍历比对。
┌─────────────────────────────────────────────────────────────┐ │ 快照沙箱运行生命周期 │ ├──────────────────────────────┬──────────────────────────────┤ │ 1. 激活 (Mount) │ 遍历当前 window 记录快照快照A │ │ │ 还原上一次记录的修改差异表 │ ├──────────────────────────────┼──────────────────────────────┤ │ 2. 运行中 │ 子应用直接读写真实的 window │ ├──────────────────────────────┼──────────────────────────────┤ │ 3. 卸载 (Unmount) │ 遍历当前 window 与快照A比对 │ │ │ 记录差异并强制将 window 复原 │ └──────────────────────────────┴──────────────────────────────┘极简手写实现快照沙箱:
export class SnapshotSandbox { private windowSnapshot: Record<string, any> = {}; private modifyPropsMap: Record<string, any> = {}; // 激活沙箱 public active() { this.windowSnapshot = {}; // 1. 记录当前宿主 window 的全部属性快照 for (const prop in window) { if (window.hasOwnProperty(prop)) { this.windowSnapshot[prop] = (window as any)[prop]; } } // 2. 恢复上一次当前子应用所做的修改 for (const prop in this.modifyPropsMap) { (window as any)[prop] = this.modifyPropsMap[prop]; } } // 退出沙箱 public inactive() { this.modifyPropsMap = {}; // 遍历当前 window,揪出被修改的属性并恢复初始值 for (const prop in window) { if (window.hasOwnProperty(prop)) { if ((window as any)[prop] !== this.windowSnapshot[prop]) { // 记录被子应用篡改过的值 this.modifyPropsMap[prop] = (window as any)[prop]; // 还原主应用原始值 (window as any)[prop] = this.windowSnapshot[prop]; } } } } }快照沙箱的致命缺陷:
- 性能低下:每次切换子应用都需要
for...in遍历成千上万个window属性,耗时严重; - 天然不支持多实例(Multiple Instances):因为所有修改直接发生在同一个物理
window上,无法在同一个页面中同时渲染两个子应用。
方案二:基于 ES6 Proxy 的代理沙箱(现代微前端主力)
Proxy 沙箱不直接篡改全局物理window,而是为每一个子应用创建一个专属的fakeWindow代理上下文。
子应用的代码在执行时,通过with (fakeWindow)或闭包绑定,所有的window.xxx = 123全部被 Proxy 拦截并写入私有的fakeWindow中,真实的全局window永远保持纯净!
子应用 A (window.x = 1) ──► [ Proxy 拦截 ] ──► 写入 fakeWindowA │ (物理 window.x 仍为 undefined) 子应用 B (window.x = 2) ──► [ Proxy 拦截 ] ──► 写入 fakeWindowB极简手写实现 Proxy 多实例单应用沙箱:
export class ProxySandbox { public proxy: Window; public isRunning = false; private fakeWindow: Record<string, any> = {}; constructor(public name: string) { const rawWindow = window; const fakeWindow = this.fakeWindow; this.proxy = new Proxy(fakeWindow, { set: (target, prop: string, value: any) => { if (this.isRunning) { // 子应用的赋值操作全部写入独立的 fakeWindow target[prop] = value; return true; } return true; }, get: (target, prop: string) => { // 优先从子应用自身的 fakeWindow 中读取 if (prop in target) { return target[prop]; } // 否则回退到宿主物理 window 中查找(如 document, location, setTimeout) const value = (rawWindow as any)[prop]; if (typeof value === 'function') { // 核心细节:绑定 this 指针到原生 window,防止非法调用异常 return value.bind(rawWindow); } return value; }, has: (target, prop: string) => { return prop in target || prop in rawWindow; } }) as Window; } public active() { this.isRunning = true; } public inactive() { this.isRunning = false; this.fakeWindow = {}; // 清理当前子应用的私有变量 } }执行上下文绑定:动态代码执行
在加载子应用打包出来的 JS 脚本时,微前端通过闭包立即执行函数(IIFE)并将window重定向到沙箱proxy:
export function runScriptInSandbox(scriptContent: string, sandbox: ProxySandbox) { sandbox.active(); // 利用 with + Function 将全局顶层标识符限制在 proxy 沙箱内 const executableFunction = new Function('window', 'self', 'globalThis', ` with(window) { ${scriptContent} } `); executableFunction(sandbox.proxy, sandbox.proxy, sandbox.proxy); }沙箱技术总结
理解了 Proxy 沙箱与快照沙箱的底层原理,在排查微前端中的跨应用变量污染、定时器残留和全局事件解绑失败等疑难杂症时,就能一眼看穿本质,游刃有余。