乾坤(qiankun)沙箱机制学习记录

概述

乾坤(qiankun)是阿里巴巴开源的微前端框架,基于 single-spa 封装,提供了开箱即用的微前端解决方案。

其最核心的技术之一就是沙箱机制,沙箱机制旨在确保多个微应用同时运行时,它们的 JavaScript 执行和 CSS 样式不会相互污染,从而提供一个安全、隔离的运行时环境。

什么是沙箱?

沙箱(Sandbox)是一种安全机制,用于在受控环境中执行代码,防止恶意或有问题的代码影响系统的其他部分,沙箱主要分为两大类:JS 沙箱样式(CSS)沙箱

在微前端场景中,沙箱主要解决以下问题:

  • 全局变量污染:防止不同微应用的全局变量相互影响
  • DOM 污染:隔离 DOM 操作,防止样式和事件冲突
  • 副作用管理:确保微应用卸载时能完全清理副作用
  • 运行时隔离:为每个微应用提供独立的执行环境

乾坤沙箱演进历程

乾坤的沙箱机制经历了几个版本的演进:

1. SnapshotSandbox(快照沙箱)

  • 适用场景:不支持 Proxy 的旧版浏览器
  • 实现原理:通过记录和恢复 window 对象的快照
  • 优缺点:兼容性好但性能较差,无法支持多实例

2. LegacySandbox(遗留沙箱)

  • 适用场景:支持 Proxy 但不支持多实例的场景
  • 实现原理:使用 Proxy 代理 window 对象
  • 优缺点:性能好但仍然无法支持多实例

3. ProxySandbox(代理沙箱)

  • 适用场景:现代浏览器,支持多实例
  • 实现原理:为每个微应用创建独立的代理对象
  • 优缺点:性能最佳,支持多实例并发

沙箱实现原理详解

1. SnapshotSandbox 快照沙箱

快照沙箱是最简单的实现方式,通过在微应用激活前记录 window 状态,卸载时恢复状态来实现隔离。

  1. /**
  2. * 快照沙箱实现
  3. */
  4. class SnapshotSandbox {
  5. constructor(name) {
  6. this.name = name
  7. this.modifyPropsMap = {} // 记录被修改的属性
  8. this.active = false
  9. }
  10. /**
  11. * 激活沙箱
  12. */
  13. active() {
  14. if (this.active) return
  15. // 记录当前 window 状态快照
  16. this.windowSnapshot = {}
  17. // 遍历 window 对象的所有属性
  18. for (const prop in window) {
  19. if (window.hasOwnProperty(prop)) {
  20. this.windowSnapshot[prop] = window[prop]
  21. }
  22. }
  23. // 注意这里是 modifyPropsMap,在组件被二次激活时,需要恢复之前的修改
  24. Object.keys(this.modifyPropsMap).forEach(p => {
  25. window[p] = this.modifyPropsMap[p]
  26. })
  27. this.active = true
  28. }
  29. /**
  30. * 失活沙箱
  31. */
  32. inactive() {
  33. if (!this.active) return
  34. // 记录被修改的属性
  35. this.modifyPropsMap = {}
  36. // 比较当前 window 与快照的差异
  37. for (const prop in window) {
  38. if (window.hasOwnProperty(prop)) {
  39. if (window[prop] !== this.windowSnapshot[prop]) {
  40. // 记录被修改的属性,以便组件再次被激活时,反向恢复属性
  41. this.modifyPropsMap[prop] = window[prop]
  42. // 恢复原始值,使用记录的镜像值覆盖被修改的属性
  43. window[prop] = this.windowSnapshot[prop]
  44. }
  45. }
  46. }
  47. // 清理新增的属性
  48. for (const prop in window) {
  49. if (window.hasOwnProperty(prop) && !(prop in this.windowSnapshot)) {
  50. // 记录新增的属性(值为 undefined 表示需要删除?)
  51. // this.modifyPropsMap[prop] = undefined
  52. delete window[prop]
  53. }
  54. }
  55. this.active = false
  56. }
  57. }
  58. // 使用示例
  59. const sandbox = new SnapshotSandbox('app1')
  60. // 激活沙箱
  61. sandbox.active()
  62. // 微应用在沙箱中运行,但是实际更改的还是window对象的属性,只是在沙箱失活时会将其恢复
  63. window.customGlobalVar = 'app1-value'
  64. window.APP_NAME = 'MicroApp1'
  65. // 失活沙箱(自动恢复 window 状态)
  66. sandbox.inactive()
  67. // 沙箱失活后,快照沙箱会自动清理所有属性
  68. console.log(window.customGlobalVar) // undefined
  69. console.log(window.APP_NAME) // undefined
JavaScript复制

快照沙箱的特点

优点:

  • 实现简单,容易理解
  • 兼容性好,支持所有浏览器
  • 能完全隔离 window 对象的修改

缺点:

  • 性能较差,需要遍历整个 window 对象
  • 无法支持多实例并行运行
  • 对于属性很多的 window 对象,内存占用较大

2. LegacySandbox 遗留沙箱

遗留沙箱使用 ES6 的 Proxy 特性来代理 window 对象,相比快照沙箱有更好的性能。

  1. /**
  2. * 遗留沙箱实现
  3. */
  4. class LegacySandbox {
  5. constructor(name) {
  6. this.name = name
  7. this.active = false
  8. // 记录沙箱期间新增的全局变量
  9. this.addedPropsMapInSandbox = new Map()
  10. // 记录沙箱期间被修改的全局变量
  11. this.modifiedPropsOriginalValueMapInSandbox = new Map()
  12. // 记录沙箱期间被修改但在沙箱外部也被修改的全局变量
  13. this.currentUpdatedPropsValueMapInSandbox = new Map()
  14. // 创建 Proxy 代理
  15. this.proxy = new Proxy(window, {
  16. get: (target, prop) => {
  17. return target[prop]
  18. },
  19. set: (target, prop, value) => {
  20. if (this.active) {
  21. // 如果当前属性不存在,记录为新增属性
  22. if (!(prop in target)) {
  23. this.addedPropsMapInSandbox.set(prop, value)
  24. }
  25. // 如果当前属性存在且之前没有被修改过,记录原始值
  26. else if (!this.modifiedPropsOriginalValueMapInSandbox.has(prop)) {
  27. this.modifiedPropsOriginalValueMapInSandbox.set(prop, target[prop])
  28. }
  29. // 记录当前修改的值
  30. this.currentUpdatedPropsValueMapInSandbox.set(prop, value)
  31. }
  32. // 设置属性值
  33. target[prop] = value
  34. return true
  35. },
  36. deleteProperty: (target, prop) => {
  37. if (this.active) {
  38. // 如果属性存在,记录删除操作
  39. if (prop in target) {
  40. this.addedPropsMapInSandbox.delete(prop)
  41. this.currentUpdatedPropsValueMapInSandbox.delete(prop)
  42. if (!this.modifiedPropsOriginalValueMapInSandbox.has(prop)) {
  43. this.modifiedPropsOriginalValueMapInSandbox.set(prop, target[prop])
  44. }
  45. }
  46. }
  47. delete target[prop]
  48. return true
  49. }
  50. })
  51. }
  52. /**
  53. * 激活沙箱
  54. */
  55. active() {
  56. if (this.active) return
  57. // 恢复沙箱期间的修改
  58. this.currentUpdatedPropsValueMapInSandbox.forEach((value, prop) => {
  59. window[prop] = value
  60. })
  61. this.active = true
  62. }
  63. /**
  64. * 失活沙箱
  65. */
  66. inactive() {
  67. if (!this.active) return
  68. // 删除新增的属性
  69. this.addedPropsMapInSandbox.forEach((_, prop) => {
  70. delete window[prop]
  71. })
  72. // 恢复被修改属性的原始值
  73. this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop) => {
  74. window[prop] = value
  75. })
  76. this.active = false
  77. }
  78. }
  79. // 使用示例
  80. const legacySandbox = new LegacySandbox('app1')
  81. // 激活沙箱
  82. legacySandbox.active()
  83. // 通过代理对象操作全局变量
  84. legacySandbox.proxy.customVar = 'legacy-value'
  85. legacySandbox.proxy.existingVar = 'modified-value'
  86. // 失活沙箱
  87. legacySandbox.inactive()
  88. console.log(window.customVar) // undefined
  89. console.log(window.existingVar) // 恢复原始值
JavaScript复制

遗留沙箱的特点

优点:

  • 性能比快照沙箱好,只记录变化的属性
  • 使用 Proxy 可以精确拦截属性操作
  • 支持属性的增删改查操作

缺点:

  • 仍然直接操作 window 对象,无法支持多实例
  • 需要浏览器支持 Proxy(IE11+ 及现代浏览器)

3. ProxySandbox 代理沙箱

代理沙箱是乾坤最新的沙箱实现,为每个微应用创建一个独立的代理对象,真正实现了多实例隔离。

  1. /**
  2. * 代理沙箱实现
  3. */
  4. class ProxySandbox {
  5. constructor(name) {
  6. this.name = name
  7. this.active = false
  8. // 沙箱的私有变量存储
  9. this.fakeWindow = {}
  10. // 记录哪些属性是沙箱私有的
  11. this.propertiesWithGetter = new Map()
  12. // 创建代理对象
  13. this.proxy = new Proxy(this.fakeWindow, {
  14. get: (target, prop) => {
  15. // 如果沙箱处于激活状态
  16. if (this.active) {
  17. // 优先从沙箱私有变量中获取
  18. if (prop in target) {
  19. return target[prop]
  20. }
  21. // 如果沙箱中没有,则从 window 获取
  22. const value = window[prop]
  23. // 如果是函数,需要绑定正确的 this
  24. if (typeof value === 'function') {
  25. // 对于 window 上的函数,绑定 window 作为 this
  26. const boundValue = value.bind(window)
  27. // 缓存绑定后的函数,避免重复绑定
  28. target[prop] = boundValue
  29. return boundValue
  30. }
  31. return value
  32. }
  33. // 沙箱未激活时,直接返回 window 上的值
  34. return window[prop]
  35. },
  36. set: (target, prop, value) => {
  37. if (this.active) {
  38. // 检查是否是 window 的原生属性
  39. if (this.isWindowProperty(prop)) {
  40. // 如果是 window 的原生属性,直接设置到 window
  41. window[prop] = value
  42. } else {
  43. // 否则设置到沙箱的私有空间
  44. target[prop] = value
  45. }
  46. } else {
  47. // 沙箱未激活时,直接设置到 window
  48. window[prop] = value
  49. }
  50. return true
  51. },
  52. deleteProperty: (target, prop) => {
  53. if (this.active) {
  54. if (prop in target) {
  55. delete target[prop]
  56. } else if (prop in window) {
  57. delete window[prop]
  58. }
  59. } else {
  60. delete window[prop]
  61. }
  62. return true
  63. },
  64. has: (target, prop) => {
  65. return prop in target || prop in window
  66. },
  67. ownKeys: (target) => {
  68. return [...Object.getOwnPropertyNames(target), ...Object.getOwnPropertyNames(window)]
  69. },
  70. getOwnPropertyDescriptor: (target, prop) => {
  71. if (prop in target) {
  72. return Object.getOwnPropertyDescriptor(target, prop)
  73. }
  74. if (prop in window) {
  75. return Object.getOwnPropertyDescriptor(window, prop)
  76. }
  77. return undefined
  78. }
  79. })
  80. }
  81. /**
  82. * 判断属性是否为 window 的原生属性
  83. */
  84. isWindowProperty(prop) {
  85. // 这里可以根据具体需求判断
  86. // 例如:document, location, navigator 等应该直接操作 window
  87. const windowProperties = [
  88. 'document', 'location', 'navigator', 'history', 'screen',
  89. 'alert', 'confirm', 'prompt', 'setTimeout', 'clearTimeout',
  90. 'setInterval', 'clearInterval', 'requestAnimationFrame',
  91. 'cancelAnimationFrame', 'fetch', 'XMLHttpRequest'
  92. ]
  93. return windowProperties.includes(prop) ||
  94. prop.startsWith('webkit') ||
  95. prop.startsWith('moz') ||
  96. prop.startsWith('ms')
  97. }
  98. /**
  99. * 激活沙箱
  100. */
  101. active() {
  102. this.active = true
  103. }
  104. /**
  105. * 失活沙箱
  106. */
  107. inactive() {
  108. this.active = false
  109. }
  110. /**
  111. * 获取沙箱的全局代理对象
  112. */
  113. getProxy() {
  114. return this.proxy
  115. }
  116. }
  117. // 使用示例
  118. const proxySandbox1 = new ProxySandbox('app1')
  119. const proxySandbox2 = new ProxySandbox('app2')
  120. // 激活两个沙箱
  121. proxySandbox1.active()
  122. proxySandbox2.active()
  123. // 在不同沙箱中设置同名变量
  124. const proxy1 = proxySandbox1.getProxy()
  125. const proxy2 = proxySandbox2.getProxy()
  126. proxy1.customVar = 'value-from-app1'
  127. proxy2.customVar = 'value-from-app2'
  128. console.log(proxy1.customVar) // 'value-from-app1'
  129. console.log(proxy2.customVar) // 'value-from-app2'
  130. console.log(window.customVar) // undefined
  131. // 访问 window 原生属性
  132. console.log(proxy1.document === window.document) // true
  133. console.log(proxy2.location === window.location) // true
JavaScript复制

代理沙箱的特点

优点:

  • 真正实现了多实例隔离,多个微应用可以并行运行
  • 性能最佳,每个沙箱只维护自己的变量
  • 提供了最完整的 JavaScript 沙箱能力

缺点:

  • 实现复杂度最高
  • 需要浏览器支持 Proxy 和 Reflect

乾坤沙箱源码学习

沙箱工厂函数

乾坤根据浏览器能力和配置自动选择合适的沙箱实现:

  1. /**
  2. * 乾坤沙箱创建工厂
  3. */
  4. function createSandbox(appName, elementGetter, scopedCSS, useLooseSandbox) {
  5. let sandbox
  6. // 检查浏览器对 Proxy 的支持
  7. if (window.Proxy) {
  8. // 如果需要严格沙箱且支持 Proxy,使用 ProxySandbox
  9. if (!useLooseSandbox) {
  10. sandbox = new ProxySandbox(appName)
  11. } else {
  12. // 否则使用 LegacySandbox(向后兼容)
  13. sandbox = new LegacySandbox(appName)
  14. }
  15. } else {
  16. // 不支持 Proxy 的情况下使用 SnapshotSandbox
  17. sandbox = new SnapshotSandbox(appName)
  18. }
  19. return sandbox
  20. }
  21. /**
  22. * 沙箱管理器
  23. */
  24. class SandboxManager {
  25. constructor() {
  26. this.sandboxes = new Map()
  27. }
  28. /**
  29. * 创建沙箱
  30. */
  31. createSandbox(appName, options = {}) {
  32. if (this.sandboxes.has(appName)) {
  33. return this.sandboxes.get(appName)
  34. }
  35. const sandbox = createSandbox(
  36. appName,
  37. options.elementGetter,
  38. options.scopedCSS,
  39. options.useLooseSandbox
  40. )
  41. this.sandboxes.set(appName, sandbox)
  42. return sandbox
  43. }
  44. /**
  45. * 激活沙箱
  46. */
  47. activateSandbox(appName) {
  48. const sandbox = this.sandboxes.get(appName)
  49. if (sandbox) {
  50. sandbox.active()
  51. return sandbox.proxy || window
  52. }
  53. return window
  54. }
  55. /**
  56. * 失活沙箱
  57. */
  58. deactivateSandbox(appName) {
  59. const sandbox = this.sandboxes.get(appName)
  60. if (sandbox) {
  61. sandbox.inactive()
  62. }
  63. }
  64. /**
  65. * 销毁沙箱
  66. */
  67. destroySandbox(appName) {
  68. const sandbox = this.sandboxes.get(appName)
  69. if (sandbox) {
  70. sandbox.inactive()
  71. this.sandboxes.delete(appName)
  72. }
  73. }
  74. }
  75. // 乾坤全局沙箱管理器
  76. const sandboxManager = new SandboxManager()
  77. export { sandboxManager, createSandbox }
JavaScript复制

沙箱与微应用生命周期整合

  1. /**
  2. * 微应用生命周期管理
  3. */
  4. class MicroAppLifecycle {
  5. constructor(appName, options) {
  6. this.appName = appName
  7. this.options = options
  8. this.sandbox = null
  9. this.globalProxy = null
  10. }
  11. /**
  12. * 微应用挂载前
  13. */
  14. async beforeMount() {
  15. // 创建沙箱
  16. this.sandbox = sandboxManager.createSandbox(this.appName, this.options)
  17. // 激活沙箱并获取全局代理对象
  18. this.globalProxy = sandboxManager.activateSandbox(this.appName)
  19. console.log(`${this.appName} 沙箱已激活`)
  20. }
  21. /**
  22. * 微应用挂载
  23. */
  24. async mount() {
  25. // 在沙箱环境中执行微应用的挂载逻辑
  26. const originalWindow = window
  27. try {
  28. // 临时替换全局 window 对象
  29. if (this.globalProxy && this.globalProxy !== window) {
  30. // 这里需要通过特殊方式注入 globalProxy
  31. // 实际实现会更复杂,涉及到模块加载器的改造
  32. }
  33. // 执行微应用挂载逻辑
  34. await this.executeAppMount()
  35. } finally {
  36. // 恢复原始 window 对象
  37. // window = originalWindow
  38. }
  39. }
  40. /**
  41. * 微应用卸载
  42. */
  43. async unmount() {
  44. // 执行微应用卸载逻辑
  45. await this.executeAppUnmount()
  46. // 失活沙箱
  47. sandboxManager.deactivateSandbox(this.appName)
  48. console.log(`${this.appName} 沙箱已失活`)
  49. }
  50. /**
  51. * 微应用销毁
  52. */
  53. async destroy() {
  54. // 销毁沙箱
  55. sandboxManager.destroySandbox(this.appName)
  56. console.log(`${this.appName} 沙箱已销毁`)
  57. }
  58. /**
  59. * 执行微应用挂载(需要在沙箱环境中)
  60. */
  61. async executeAppMount() {
  62. // 这里会加载并执行微应用的 JavaScript 代码
  63. // 实际实现涉及到模块加载、代码执行等复杂逻辑
  64. }
  65. /**
  66. * 执行微应用卸载
  67. */
  68. async executeAppUnmount() {
  69. // 这里会执行微应用的清理逻辑
  70. }
  71. }
  72. // 使用示例
  73. const microApp = new MicroAppLifecycle('my-micro-app', {
  74. useLooseSandbox: false,
  75. scopedCSS: true
  76. })
  77. // 微应用生命周期调用
  78. async function runMicroApp() {
  79. await microApp.beforeMount()
  80. await microApp.mount()
  81. // 模拟应用运行一段时间
  82. setTimeout(async () => {
  83. await microApp.unmount()
  84. await microApp.destroy()
  85. }, 5000)
  86. }
JavaScript复制

沙箱的高级特性

1. 副作用清理

乾坤沙箱不仅隔离全局变量,还会自动清理微应用产生的副作用:

  1. /**
  2. * 副作用收集器
  3. */
  4. class SideEffectCollector {
  5. constructor() {
  6. this.effects = []
  7. }
  8. /**
  9. * 收集定时器副作用
  10. */
  11. patchTimer() {
  12. const originalSetTimeout = window.setTimeout
  13. const originalSetInterval = window.setInterval
  14. const originalClearTimeout = window.clearTimeout
  15. const originalClearInterval = window.clearInterval
  16. const timers = new Set()
  17. // 代理 setTimeout
  18. window.setTimeout = (callback, delay, ...args) => {
  19. const timer = originalSetTimeout(callback, delay, ...args)
  20. timers.add(timer)
  21. return timer
  22. }
  23. // 代理 setInterval
  24. window.setInterval = (callback, delay, ...args) => {
  25. const timer = originalSetInterval(callback, delay, ...args)
  26. timers.add(timer)
  27. return timer
  28. }
  29. // 代理 clearTimeout
  30. window.clearTimeout = (timer) => {
  31. timers.delete(timer)
  32. return originalClearTimeout(timer)
  33. }
  34. // 代理 clearInterval
  35. window.clearInterval = (timer) => {
  36. timers.delete(timer)
  37. return originalClearInterval(timer)
  38. }
  39. // 记录恢复函数
  40. this.effects.push(() => {
  41. // 清理所有未清理的定时器
  42. timers.forEach(timer => {
  43. originalClearTimeout(timer)
  44. originalClearInterval(timer)
  45. })
  46. // 恢复原始函数
  47. window.setTimeout = originalSetTimeout
  48. window.setInterval = originalSetInterval
  49. window.clearTimeout = originalClearTimeout
  50. window.clearInterval = originalClearInterval
  51. })
  52. }
  53. /**
  54. * 收集事件监听器副作用
  55. */
  56. patchEventListener() {
  57. const originalAddEventListener = window.addEventListener
  58. const originalRemoveEventListener = window.removeEventListener
  59. const listeners = new Map()
  60. // 代理 addEventListener
  61. window.addEventListener = (type, listener, options) => {
  62. const key = { type, listener, options }
  63. listeners.set(key, { type, listener, options })
  64. return originalAddEventListener.call(window, type, listener, options)
  65. }
  66. // 代理 removeEventListener
  67. window.removeEventListener = (type, listener, options) => {
  68. const key = Array.from(listeners.keys()).find(k =>
  69. k.type === type && k.listener === listener && k.options === options
  70. )
  71. if (key) {
  72. listeners.delete(key)
  73. }
  74. return originalRemoveEventListener.call(window, type, listener, options)
  75. }
  76. // 记录恢复函数
  77. this.effects.push(() => {
  78. // 清理所有未清理的事件监听器
  79. listeners.forEach(({ type, listener, options }) => {
  80. originalRemoveEventListener.call(window, type, listener, options)
  81. })
  82. // 恢复原始函数
  83. window.addEventListener = originalAddEventListener
  84. window.removeEventListener = originalRemoveEventListener
  85. })
  86. }
  87. /**
  88. * 收集 DOM 变更副作用
  89. */
  90. patchDOMChanges() {
  91. const addedNodes = new Set()
  92. const modifiedAttributes = new Map()
  93. // 监控 DOM 变更
  94. const observer = new MutationObserver(mutations => {
  95. mutations.forEach(mutation => {
  96. if (mutation.type === 'childList') {
  97. mutation.addedNodes.forEach(node => {
  98. if (node.nodeType === Node.ELEMENT_NODE) {
  99. addedNodes.add(node)
  100. }
  101. })
  102. } else if (mutation.type === 'attributes') {
  103. if (!modifiedAttributes.has(mutation.target)) {
  104. modifiedAttributes.set(mutation.target, new Map())
  105. }
  106. modifiedAttributes.get(mutation.target).set(
  107. mutation.attributeName,
  108. mutation.oldValue
  109. )
  110. }
  111. })
  112. })
  113. observer.observe(document, {
  114. childList: true,
  115. subtree: true,
  116. attributes: true,
  117. attributeOldValue: true
  118. })
  119. // 记录恢复函数
  120. this.effects.push(() => {
  121. // 停止监控
  122. observer.disconnect()
  123. // 清理新增的 DOM 节点
  124. addedNodes.forEach(node => {
  125. if (node.parentNode) {
  126. node.parentNode.removeChild(node)
  127. }
  128. })
  129. // 恢复被修改的属性
  130. modifiedAttributes.forEach((attrs, element) => {
  131. attrs.forEach((oldValue, attrName) => {
  132. if (oldValue === null) {
  133. element.removeAttribute(attrName)
  134. } else {
  135. element.setAttribute(attrName, oldValue)
  136. }
  137. })
  138. })
  139. })
  140. }
  141. /**
  142. * 应用所有补丁
  143. */
  144. applyPatches() {
  145. this.patchTimer()
  146. this.patchEventListener()
  147. this.patchDOMChanges()
  148. }
  149. /**
  150. * 清理所有副作用
  151. */
  152. cleanup() {
  153. this.effects.forEach(effect => effect())
  154. this.effects = []
  155. }
  156. }
  157. /**
  158. * 增强的代理沙箱,集成副作用管理
  159. */
  160. class EnhancedProxySandbox extends ProxySandbox {
  161. constructor(name) {
  162. super(name)
  163. this.sideEffectCollector = new SideEffectCollector()
  164. }
  165. active() {
  166. super.active()
  167. // 应用副作用收集补丁
  168. this.sideEffectCollector.applyPatches()
  169. }
  170. inactive() {
  171. // 清理副作用
  172. this.sideEffectCollector.cleanup()
  173. super.inactive()
  174. }
  175. }
JavaScript复制

2. CSS 隔离

乾坤也提供了 CSS 隔离机制:

  1. /**
  2. * CSS 沙箱实现
  3. */
  4. class CSSSandbox {
  5. constructor(appName, container) {
  6. this.appName = appName
  7. this.container = container
  8. this.dynamicStyleSheets = new Set()
  9. this.modifiedStyles = new Map()
  10. }
  11. /**
  12. * 激活 CSS 沙箱
  13. */
  14. activate() {
  15. this.patchDynamicCSS()
  16. this.scopeStaticCSS()
  17. }
  18. /**
  19. * 处理动态添加的样式
  20. */
  21. patchDynamicCSS() {
  22. const originalCreateElement = document.createElement
  23. const originalAppendChild = Node.prototype.appendChild
  24. const originalInsertBefore = Node.prototype.insertBefore
  25. // 代理 createElement
  26. document.createElement = (tagName) => {
  27. const element = originalCreateElement.call(document, tagName)
  28. if (tagName.toLowerCase() === 'style') {
  29. this.dynamicStyleSheets.add(element)
  30. }
  31. return element
  32. }
  33. // 代理 appendChild
  34. Node.prototype.appendChild = function(child) {
  35. if (child.tagName === 'STYLE' || child.tagName === 'LINK') {
  36. this.dynamicStyleSheets.add(child)
  37. // 为动态样式添加作用域
  38. this.scopeStyleSheet(child)
  39. }
  40. return originalAppendChild.call(this, child)
  41. }
  42. // 记录原始函数用于恢复
  43. this.restoreFunctions = () => {
  44. document.createElement = originalCreateElement
  45. Node.prototype.appendChild = originalAppendChild
  46. Node.prototype.insertBefore = originalInsertBefore
  47. }
  48. }
  49. /**
  50. * 为样式表添加作用域
  51. */
  52. scopeStyleSheet(styleElement) {
  53. // 添加特殊的属性前缀,防止样式冲突
  54. const appSelector = `[data-qiankun="${this.appName}"]`
  55. if (styleElement.sheet) {
  56. const rules = Array.from(styleElement.sheet.cssRules || styleElement.sheet.rules)
  57. rules.forEach((rule, index) => {
  58. if (rule.type === CSSRule.STYLE_RULE) {
  59. const originalSelector = rule.selectorText
  60. const scopedSelector = `${appSelector} ${originalSelector}`
  61. // 删除原规则并添加带作用域的新规则
  62. styleElement.sheet.deleteRule(index)
  63. styleElement.sheet.insertRule(`${scopedSelector} { ${rule.style.cssText} }`, index)
  64. }
  65. })
  66. }
  67. }
  68. /**
  69. * 处理静态 CSS
  70. */
  71. scopeStaticCSS() {
  72. const styleSheets = Array.from(document.styleSheets)
  73. styleSheets.forEach(sheet => {
  74. try {
  75. if (sheet.ownerNode && this.isAppStyleSheet(sheet.ownerNode)) {
  76. this.scopeStyleSheet(sheet.ownerNode)
  77. }
  78. } catch (e) {
  79. // 跨域样式表无法访问
  80. console.warn('无法处理跨域样式表:', e)
  81. }
  82. })
  83. }
  84. /**
  85. * 判断是否为当前应用的样式表
  86. */
  87. isAppStyleSheet(element) {
  88. // 根据元素位置或其他标识判断是否为当前应用的样式
  89. return this.container.contains(element)
  90. }
  91. /**
  92. * 失活 CSS 沙箱
  93. */
  94. deactivate() {
  95. // 恢复原始函数
  96. if (this.restoreFunctions) {
  97. this.restoreFunctions()
  98. }
  99. // 移除动态添加的样式
  100. this.dynamicStyleSheets.forEach(element => {
  101. if (element.parentNode) {
  102. element.parentNode.removeChild(element)
  103. }
  104. })
  105. this.dynamicStyleSheets.clear()
  106. }
  107. }
JavaScript复制

实际应用场景

1. 电商平台微前端架构

  1. /**
  2. * 电商平台微前端沙箱配置
  3. */
  4. class ECommerceSandboxConfig {
  5. static getConfig(appName) {
  6. const configs = {
  7. // 主应用 - 不需要沙箱
  8. 'main-app': {
  9. sandbox: false
  10. },
  11. // 用户中心 - 需要严格隔离
  12. 'user-center': {
  13. sandbox: true,
  14. useLooseSandbox: false,
  15. scopedCSS: true,
  16. customProps: {
  17. // 共享用户信息
  18. userInfo: window.globalUserInfo,
  19. // 共享通用工具
  20. utils: window.commonUtils
  21. }
  22. },
  23. // 商品模块 - 允许宽松沙箱以提升性能
  24. 'product-module': {
  25. sandbox: true,
  26. useLooseSandbox: true,
  27. scopedCSS: true,
  28. customProps: {
  29. productAPI: window.productAPI
  30. }
  31. },
  32. // 支付模块 - 最严格的隔离
  33. 'payment-module': {
  34. sandbox: true,
  35. useLooseSandbox: false,
  36. scopedCSS: true,
  37. singular: true, // 单例模式
  38. customProps: {
  39. paymentConfig: window.securePaymentConfig
  40. }
  41. }
  42. }
  43. return configs[appName] || { sandbox: true }
  44. }
  45. }
  46. /**
  47. * 微前端应用管理器
  48. */
  49. class MicroAppManager {
  50. constructor() {
  51. this.apps = new Map()
  52. this.sandboxManager = new SandboxManager()
  53. }
  54. /**
  55. * 注册微应用
  56. */
  57. registerApp(appConfig) {
  58. const { name, entry, container, activeRule } = appConfig
  59. const sandboxConfig = ECommerceSandboxConfig.getConfig(name)
  60. this.apps.set(name, {
  61. ...appConfig,
  62. sandboxConfig,
  63. status: 'registered'
  64. })
  65. }
  66. /**
  67. * 加载并挂载应用
  68. */
  69. async mountApp(appName) {
  70. const appConfig = this.apps.get(appName)
  71. if (!appConfig) {
  72. throw new Error(`应用 ${appName} 未注册`)
  73. }
  74. try {
  75. // 创建沙箱
  76. if (appConfig.sandboxConfig.sandbox) {
  77. const sandbox = this.sandboxManager.createSandbox(appName, appConfig.sandboxConfig)
  78. const globalProxy = this.sandboxManager.activateSandbox(appName)
  79. // 注入自定义属性
  80. if (appConfig.sandboxConfig.customProps) {
  81. Object.keys(appConfig.sandboxConfig.customProps).forEach(key => {
  82. globalProxy[key] = appConfig.sandboxConfig.customProps[key]
  83. })
  84. }
  85. }
  86. // 加载应用资源
  87. const { template, execScripts } = await this.loadApp(appConfig.entry)
  88. // 渲染应用模板
  89. const container = document.querySelector(appConfig.container)
  90. container.innerHTML = template
  91. // 在沙箱环境中执行应用脚本
  92. const appExports = await execScripts(
  93. appConfig.sandboxConfig.sandbox ?
  94. this.sandboxManager.sandboxes.get(appName).proxy :
  95. window
  96. )
  97. // 执行应用挂载
  98. if (appExports.mount) {
  99. await appExports.mount({
  100. container,
  101. props: appConfig.sandboxConfig.customProps || {}
  102. })
  103. }
  104. appConfig.status = 'mounted'
  105. appConfig.appExports = appExports
  106. } catch (error) {
  107. console.error(`挂载应用 ${appName} 失败:`, error)
  108. appConfig.status = 'load_error'
  109. }
  110. }
  111. /**
  112. * 卸载应用
  113. */
  114. async unmountApp(appName) {
  115. const appConfig = this.apps.get(appName)
  116. if (!appConfig || appConfig.status !== 'mounted') {
  117. return
  118. }
  119. try {
  120. // 执行应用卸载
  121. if (appConfig.appExports && appConfig.appExports.unmount) {
  122. await appConfig.appExports.unmount()
  123. }
  124. // 失活沙箱
  125. if (appConfig.sandboxConfig.sandbox) {
  126. this.sandboxManager.deactivateSandbox(appName)
  127. }
  128. // 清理容器
  129. const container = document.querySelector(appConfig.container)
  130. container.innerHTML = ''
  131. appConfig.status = 'unmounted'
  132. } catch (error) {
  133. console.error(`卸载应用 ${appName} 失败:`, error)
  134. }
  135. }
  136. /**
  137. * 加载应用资源
  138. */
  139. async loadApp(entry) {
  140. // 这里实现应用资源加载逻辑
  141. // 包括 HTML、CSS、JavaScript 的获取和处理
  142. const response = await fetch(entry)
  143. const html = await response.text()
  144. // 解析 HTML,提取 CSS 和 JS
  145. const { template, scripts, styles } = this.parseHTML(html)
  146. // 创建脚本执行函数
  147. const execScripts = (global = window) => {
  148. return new Promise((resolve) => {
  149. const exports = {}
  150. scripts.forEach(script => {
  151. // 在指定的全局环境中执行脚本
  152. const func = new Function('window', 'exports', script)
  153. func.call(global, global, exports)
  154. })
  155. resolve(exports)
  156. })
  157. }
  158. return { template, execScripts }
  159. }
  160. /**
  161. * 解析 HTML 内容
  162. */
  163. parseHTML(html) {
  164. // HTML 解析逻辑
  165. const template = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
  166. const scripts = []
  167. const scriptRegex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
  168. let match
  169. while ((match = scriptRegex.exec(html)) !== null) {
  170. const scriptContent = match[0].replace(/<script[^>]*>|<\/script>/gi, '')
  171. scripts.push(scriptContent)
  172. }
  173. return { template, scripts, styles: [] }
  174. }
  175. }
  176. // 使用示例
  177. const appManager = new MicroAppManager()
  178. // 注册微应用
  179. appManager.registerApp({
  180. name: 'user-center',
  181. entry: 'http://localhost:3001',
  182. container: '#user-center-container',
  183. activeRule: '/user'
  184. })
  185. appManager.registerApp({
  186. name: 'product-module',
  187. entry: 'http://localhost:3002',
  188. container: '#product-container',
  189. activeRule: '/product'
  190. })
  191. // 路由变化时挂载/卸载应用
  192. window.addEventListener('popstate', () => {
  193. const pathname = window.location.pathname
  194. if (pathname.startsWith('/user')) {
  195. appManager.mountApp('user-center')
  196. } else if (pathname.startsWith('/product')) {
  197. appManager.mountApp('product-module')
  198. }
  199. })
JavaScript复制

2. 开发环境沙箱调试

  1. /**
  2. * 沙箱调试工具
  3. */
  4. class SandboxDebugger {
  5. constructor(sandbox) {
  6. this.sandbox = sandbox
  7. this.logs = []
  8. this.startTime = Date.now()
  9. }
  10. /**
  11. * 开始监控沙箱
  12. */
  13. startMonitoring() {
  14. this.monitorProxyAccess()
  15. this.monitorSideEffects()
  16. this.monitorPerformance()
  17. }
  18. /**
  19. * 监控代理访问
  20. */
  21. monitorProxyAccess() {
  22. const originalProxy = this.sandbox.proxy
  23. this.sandbox.proxy = new Proxy(originalProxy, {
  24. get: (target, prop, receiver) => {
  25. this.log('GET', prop, Reflect.get(target, prop, receiver))
  26. return Reflect.get(target, prop, receiver)
  27. },
  28. set: (target, prop, value, receiver) => {
  29. this.log('SET', prop, value)
  30. return Reflect.set(target, prop, value, receiver)
  31. },
  32. deleteProperty: (target, prop) => {
  33. this.log('DELETE', prop)
  34. return Reflect.deleteProperty(target, prop)
  35. }
  36. })
  37. }
  38. /**
  39. * 监控副作用
  40. */
  41. monitorSideEffects() {
  42. // 监控定时器
  43. const originalSetTimeout = window.setTimeout
  44. window.setTimeout = (...args) => {
  45. this.log('SIDE_EFFECT', 'setTimeout', args[1] || 0)
  46. return originalSetTimeout.apply(window, args)
  47. }
  48. // 监控事件监听器
  49. const originalAddEventListener = window.addEventListener
  50. window.addEventListener = (...args) => {
  51. this.log('SIDE_EFFECT', 'addEventListener', args[0])
  52. return originalAddEventListener.apply(window, args)
  53. }
  54. }
  55. /**
  56. * 监控性能
  57. */
  58. monitorPerformance() {
  59. const observer = new PerformanceObserver(list => {
  60. list.getEntries().forEach(entry => {
  61. if (entry.entryType === 'measure' || entry.entryType === 'navigation') {
  62. this.log('PERFORMANCE', entry.name, entry.duration)
  63. }
  64. })
  65. })
  66. observer.observe({ entryTypes: ['measure', 'navigation'] })
  67. }
  68. /**
  69. * 记录日志
  70. */
  71. log(type, property, value) {
  72. const logEntry = {
  73. timestamp: Date.now() - this.startTime,
  74. type,
  75. property,
  76. value,
  77. stack: new Error().stack
  78. }
  79. this.logs.push(logEntry)
  80. // 开发环境下打印日志
  81. if (process.env.NODE_ENV === 'development') {
  82. console.log(`[沙箱调试] ${type}: ${property}`, value)
  83. }
  84. }
  85. /**
  86. * 生成调试报告
  87. */
  88. generateReport() {
  89. const report = {
  90. sandboxName: this.sandbox.name,
  91. duration: Date.now() - this.startTime,
  92. totalOperations: this.logs.length,
  93. operationsByType: {},
  94. performanceMetrics: {},
  95. sideEffects: []
  96. }
  97. // 统计操作类型
  98. this.logs.forEach(log => {
  99. report.operationsByType[log.type] = (report.operationsByType[log.type] || 0) + 1
  100. if (log.type === 'SIDE_EFFECT') {
  101. report.sideEffects.push(log)
  102. } else if (log.type === 'PERFORMANCE') {
  103. report.performanceMetrics[log.property] = log.value
  104. }
  105. })
  106. return report
  107. }
  108. /**
  109. * 导出调试数据
  110. */
  111. exportDebugData() {
  112. const data = {
  113. report: this.generateReport(),
  114. logs: this.logs
  115. }
  116. const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
  117. const url = URL.createObjectURL(blob)
  118. const a = document.createElement('a')
  119. a.href = url
  120. a.download = `sandbox-debug-${this.sandbox.name}-${Date.now()}.json`
  121. a.click()
  122. URL.revokeObjectURL(url)
  123. }
  124. }
  125. // 使用示例
  126. const sandbox = new ProxySandbox('debug-app')
  127. const debugger = new SandboxDebugger(sandbox)
  128. sandbox.active()
  129. debugger.startMonitoring()
  130. // 运行一段时间后生成报告
  131. setTimeout(() => {
  132. const report = debugger.generateReport()
  133. console.log('沙箱调试报告:', report)
  134. // 导出详细调试数据
  135. debugger.exportDebugData()
  136. }, 10000)
JavaScript复制

性能优化与最佳实践

1. 沙箱性能优化

  1. /**
  2. * 性能优化的沙箱实现
  3. */
  4. class OptimizedProxySandbox extends ProxySandbox {
  5. constructor(name, options = {}) {
  6. super(name)
  7. this.options = {
  8. // 是否启用属性缓存
  9. enablePropertyCache: true,
  10. // 缓存大小限制
  11. maxCacheSize: 1000,
  12. // 是否启用懒加载
  13. enableLazyLoading: true,
  14. ...options
  15. }
  16. // 属性访问缓存
  17. this.propertyCache = new Map()
  18. // 热点属性(频繁访问的属性)
  19. this.hotProperties = new Set([
  20. 'document', 'location', 'navigator', 'console',
  21. 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'
  22. ])
  23. this.optimizeProxy()
  24. }
  25. /**
  26. * 优化代理对象
  27. */
  28. optimizeProxy() {
  29. const originalProxy = this.proxy
  30. this.proxy = new Proxy(this.fakeWindow, {
  31. get: (target, prop) => {
  32. // 对于热点属性,直接返回不走复杂逻辑
  33. if (this.hotProperties.has(prop)) {
  34. return window[prop]
  35. }
  36. // 使用缓存
  37. if (this.options.enablePropertyCache && this.propertyCache.has(prop)) {
  38. return this.propertyCache.get(prop)
  39. }
  40. const value = originalProxy[prop]
  41. // 缓存结果
  42. if (this.options.enablePropertyCache && this.propertyCache.size < this.options.maxCacheSize) {
  43. this.propertyCache.set(prop, value)
  44. }
  45. return value
  46. },
  47. set: (target, prop, value) => {
  48. // 清除相关缓存
  49. if (this.options.enablePropertyCache) {
  50. this.propertyCache.delete(prop)
  51. }
  52. return Reflect.set(originalProxy, prop, value)
  53. }
  54. })
  55. }
  56. /**
  57. * 预热常用属性
  58. */
  59. preheatProperties() {
  60. const commonProperties = [
  61. 'document', 'location', 'navigator', 'console',
  62. 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
  63. 'requestAnimationFrame', 'cancelAnimationFrame',
  64. 'fetch', 'XMLHttpRequest', 'localStorage', 'sessionStorage'
  65. ]
  66. commonProperties.forEach(prop => {
  67. // 触发属性访问以填充缓存
  68. this.proxy[prop]
  69. })
  70. }
  71. /**
  72. * 清理缓存
  73. */
  74. clearCache() {
  75. this.propertyCache.clear()
  76. }
  77. active() {
  78. super.active()
  79. // 预热属性
  80. if (this.options.enablePropertyCache) {
  81. this.preheatProperties()
  82. }
  83. }
  84. inactive() {
  85. // 清理缓存
  86. this.clearCache()
  87. super.inactive()
  88. }
  89. }
JavaScript复制

2. 沙箱使用最佳实践

  1. /**
  2. * 沙箱最佳实践指南
  3. */
  4. class SandboxBestPractices {
  5. /**
  6. * 创建生产环境推荐的沙箱配置
  7. */
  8. static createProductionConfig(appName, options = {}) {
  9. return {
  10. // 根据浏览器支持情况自动选择沙箱类型
  11. sandbox: 'auto', // auto | strict | loose | false
  12. // CSS 隔离
  13. scopedCSS: true,
  14. // 预加载优化
  15. prefetch: true,
  16. // 资源缓存
  17. cache: true,
  18. // 性能监控
  19. enablePerformanceMonitor: true,
  20. // 自定义沙箱配置
  21. sandboxConfig: {
  22. enablePropertyCache: true,
  23. maxCacheSize: 500,
  24. enableLazyLoading: true
  25. },
  26. // 全局变量白名单(允许访问的全局变量)
  27. globalWhitelist: [
  28. 'document',
  29. 'location',
  30. 'navigator',
  31. 'console',
  32. // 第三方库
  33. 'React',
  34. 'Vue',
  35. 'jQuery',
  36. // 业务全局变量
  37. 'APP_CONFIG',
  38. 'USER_INFO'
  39. ],
  40. // 副作用清理配置
  41. sideEffectConfig: {
  42. // 自动清理定时器
  43. autoCleanupTimers: true,
  44. // 自动清理事件监听器
  45. autoCleanupListeners: true,
  46. // 自动清理 DOM 变更
  47. autoCleanupDOM: true,
  48. // 自动清理网络请求
  49. autoCleanupRequests: false
  50. },
  51. ...options
  52. }
  53. }
  54. /**
  55. * 沙箱错误处理
  56. */
  57. static createErrorHandler(appName) {
  58. return {
  59. onLoadError: (error) => {
  60. console.error(`[${appName}] 应用加载失败:`, error)
  61. // 上报错误
  62. this.reportError('load_error', { appName, error: error.message })
  63. },
  64. onMountError: (error) => {
  65. console.error(`[${appName}] 应用挂载失败:`, error)
  66. // 尝试降级处理
  67. this.fallbackMount(appName)
  68. },
  69. onUnmountError: (error) => {
  70. console.error(`[${appName}] 应用卸载失败:`, error)
  71. // 强制清理
  72. this.forceCleanup(appName)
  73. },
  74. onSandboxError: (error) => {
  75. console.error(`[${appName}] 沙箱错误:`, error)
  76. // 重建沙箱
  77. this.recreateSandbox(appName)
  78. }
  79. }
  80. }
  81. /**
  82. * 上报错误
  83. */
  84. static reportError(type, data) {
  85. // 发送错误信息到监控系统
  86. fetch('/api/error-report', {
  87. method: 'POST',
  88. headers: { 'Content-Type': 'application/json' },
  89. body: JSON.stringify({
  90. type,
  91. data,
  92. timestamp: Date.now(),
  93. userAgent: navigator.userAgent,
  94. url: window.location.href
  95. })
  96. }).catch(console.error)
  97. }
  98. /**
  99. * 降级挂载
  100. */
  101. static fallbackMount(appName) {
  102. console.log(`[${appName}] 尝试降级挂载`)
  103. // 使用更宽松的沙箱配置重试
  104. const fallbackConfig = {
  105. sandbox: 'loose',
  106. scopedCSS: false
  107. }
  108. // 重新挂载应用
  109. // ... 实现降级逻辑
  110. }
  111. /**
  112. * 强制清理
  113. */
  114. static forceCleanup(appName) {
  115. console.log(`[${appName}] 执行强制清理`)
  116. // 清理容器
  117. const containers = document.querySelectorAll(`[data-app="${appName}"]`)
  118. containers.forEach(container => {
  119. container.innerHTML = ''
  120. })
  121. // 清理全局状态
  122. delete window[`__MICRO_APP_${appName.toUpperCase()}__`]
  123. }
  124. /**
  125. * 重建沙箱
  126. */
  127. static recreateSandbox(appName) {
  128. console.log(`[${appName}] 重建沙箱`)
  129. // 销毁现有沙箱
  130. sandboxManager.destroySandbox(appName)
  131. // 创建新沙箱
  132. const config = this.createProductionConfig(appName)
  133. sandboxManager.createSandbox(appName, config.sandboxConfig)
  134. }
  135. }
  136. /**
  137. * 应用性能监控
  138. */
  139. class AppPerformanceMonitor {
  140. constructor(appName) {
  141. this.appName = appName
  142. this.metrics = {
  143. loadTime: 0,
  144. mountTime: 0,
  145. unmountTime: 0,
  146. memoryUsage: 0,
  147. errorCount: 0
  148. }
  149. }
  150. /**
  151. * 开始监控应用加载
  152. */
  153. startLoadMonitoring() {
  154. this.loadStartTime = performance.now()
  155. }
  156. /**
  157. * 结束加载监控
  158. */
  159. endLoadMonitoring() {
  160. this.metrics.loadTime = performance.now() - this.loadStartTime
  161. }
  162. /**
  163. * 开始挂载监控
  164. */
  165. startMountMonitoring() {
  166. this.mountStartTime = performance.now()
  167. }
  168. /**
  169. * 结束挂载监控
  170. */
  171. endMountMonitoring() {
  172. this.metrics.mountTime = performance.now() - this.mountStartTime
  173. }
  174. /**
  175. * 监控内存使用
  176. */
  177. monitorMemoryUsage() {
  178. if ('memory' in performance) {
  179. this.metrics.memoryUsage = performance.memory.usedJSHeapSize
  180. }
  181. }
  182. /**
  183. * 记录错误
  184. */
  185. recordError(error) {
  186. this.metrics.errorCount++
  187. // 上报性能数据
  188. this.reportMetrics()
  189. }
  190. /**
  191. * 上报性能指标
  192. */
  193. reportMetrics() {
  194. const data = {
  195. appName: this.appName,
  196. metrics: this.metrics,
  197. timestamp: Date.now()
  198. }
  199. console.log(`[性能监控] ${this.appName}:`, data)
  200. // 发送到监控系统
  201. fetch('/api/performance-metrics', {
  202. method: 'POST',
  203. headers: { 'Content-Type': 'application/json' },
  204. body: JSON.stringify(data)
  205. }).catch(console.error)
  206. }
  207. }
JavaScript复制

总结

乾坤的沙箱机制是微前端架构中的核心技术,它解决了多个独立应用在同一页面中运行时的隔离问题。

通过三种不同的沙箱实现,乾坤能够在不同的浏览器环境中提供最佳的隔离效果:

关键特性:

  1. 渐进式隔离:根据浏览器能力自动选择合适的沙箱实现
  2. 多实例支持:ProxySandbox 支持多个微应用同时运行
  3. 副作用管理:自动清理定时器、事件监听器等副作用
  4. 性能优化:通过缓存、预热等技术提升沙箱性能
  5. 开发友好:提供调试工具和错误处理机制

使用建议:

  • 生产环境:优先使用 ProxySandbox,启用完整的隔离和副作用管理
  • 开发环境:可以使用调试模式,便于排查问题
  • 性能敏感场景:可以考虑使用 LegacySandbox 或关闭部分隔离特性
  • 兼容性要求高:使用 SnapshotSandbox 以支持旧版浏览器
编程笔记 & 随笔杂谈