第 70 期 - qiankun 中 JS 沙箱隔离机制全解析
logoFRONTALK AI/1月1日 16:31/阅读原文

摘要

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

 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有