乾坤(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 状态,卸载时恢复状态来实现隔离。
- /**
- * 快照沙箱实现
- */
- class SnapshotSandbox {
- constructor(name) {
- this.name = name
- this.modifyPropsMap = {} // 记录被修改的属性
- this.active = false
- }
- /**
- * 激活沙箱
- */
- active() {
- if (this.active) return
- // 记录当前 window 状态快照
- this.windowSnapshot = {}
-
- // 遍历 window 对象的所有属性
- for (const prop in window) {
- if (window.hasOwnProperty(prop)) {
- this.windowSnapshot[prop] = window[prop]
- }
- }
- // 注意这里是 modifyPropsMap,在组件被二次激活时,需要恢复之前的修改
- Object.keys(this.modifyPropsMap).forEach(p => {
- window[p] = this.modifyPropsMap[p]
- })
- this.active = true
- }
- /**
- * 失活沙箱
- */
- inactive() {
- if (!this.active) return
- // 记录被修改的属性
- this.modifyPropsMap = {}
-
- // 比较当前 window 与快照的差异
- for (const prop in window) {
- if (window.hasOwnProperty(prop)) {
- if (window[prop] !== this.windowSnapshot[prop]) {
- // 记录被修改的属性,以便组件再次被激活时,反向恢复属性
- this.modifyPropsMap[prop] = window[prop]
- // 恢复原始值,使用记录的镜像值覆盖被修改的属性
- window[prop] = this.windowSnapshot[prop]
- }
- }
- }
- // 清理新增的属性
- for (const prop in window) {
- if (window.hasOwnProperty(prop) && !(prop in this.windowSnapshot)) {
- // 记录新增的属性(值为 undefined 表示需要删除?)
- // this.modifyPropsMap[prop] = undefined
- delete window[prop]
- }
- }
- this.active = false
- }
- }
- // 使用示例
- const sandbox = new SnapshotSandbox('app1')
- // 激活沙箱
- sandbox.active()
- // 微应用在沙箱中运行,但是实际更改的还是window对象的属性,只是在沙箱失活时会将其恢复
- window.customGlobalVar = 'app1-value'
- window.APP_NAME = 'MicroApp1'
- // 失活沙箱(自动恢复 window 状态)
- sandbox.inactive()
- // 沙箱失活后,快照沙箱会自动清理所有属性
- console.log(window.customGlobalVar) // undefined
- console.log(window.APP_NAME) // undefined
JavaScript复制
快照沙箱的特点
优点:
- 实现简单,容易理解
- 兼容性好,支持所有浏览器
- 能完全隔离 window 对象的修改
缺点:
- 性能较差,需要遍历整个 window 对象
- 无法支持多实例并行运行
- 对于属性很多的 window 对象,内存占用较大
2. LegacySandbox 遗留沙箱
遗留沙箱使用 ES6 的 Proxy 特性来代理 window 对象,相比快照沙箱有更好的性能。
- /**
- * 遗留沙箱实现
- */
- class LegacySandbox {
- constructor(name) {
- this.name = name
- this.active = false
-
- // 记录沙箱期间新增的全局变量
- this.addedPropsMapInSandbox = new Map()
-
- // 记录沙箱期间被修改的全局变量
- this.modifiedPropsOriginalValueMapInSandbox = new Map()
-
- // 记录沙箱期间被修改但在沙箱外部也被修改的全局变量
- this.currentUpdatedPropsValueMapInSandbox = new Map()
-
- // 创建 Proxy 代理
- this.proxy = new Proxy(window, {
- get: (target, prop) => {
- return target[prop]
- },
-
- set: (target, prop, value) => {
- if (this.active) {
- // 如果当前属性不存在,记录为新增属性
- if (!(prop in target)) {
- this.addedPropsMapInSandbox.set(prop, value)
- }
- // 如果当前属性存在且之前没有被修改过,记录原始值
- else if (!this.modifiedPropsOriginalValueMapInSandbox.has(prop)) {
- this.modifiedPropsOriginalValueMapInSandbox.set(prop, target[prop])
- }
-
- // 记录当前修改的值
- this.currentUpdatedPropsValueMapInSandbox.set(prop, value)
- }
-
- // 设置属性值
- target[prop] = value
- return true
- },
-
- deleteProperty: (target, prop) => {
- if (this.active) {
- // 如果属性存在,记录删除操作
- if (prop in target) {
- this.addedPropsMapInSandbox.delete(prop)
- this.currentUpdatedPropsValueMapInSandbox.delete(prop)
-
- if (!this.modifiedPropsOriginalValueMapInSandbox.has(prop)) {
- this.modifiedPropsOriginalValueMapInSandbox.set(prop, target[prop])
- }
- }
- }
-
- delete target[prop]
- return true
- }
- })
- }
- /**
- * 激活沙箱
- */
- active() {
- if (this.active) return
-
- // 恢复沙箱期间的修改
- this.currentUpdatedPropsValueMapInSandbox.forEach((value, prop) => {
- window[prop] = value
- })
-
- this.active = true
- }
- /**
- * 失活沙箱
- */
- inactive() {
- if (!this.active) return
-
- // 删除新增的属性
- this.addedPropsMapInSandbox.forEach((_, prop) => {
- delete window[prop]
- })
-
- // 恢复被修改属性的原始值
- this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop) => {
- window[prop] = value
- })
-
- this.active = false
- }
- }
- // 使用示例
- const legacySandbox = new LegacySandbox('app1')
- // 激活沙箱
- legacySandbox.active()
- // 通过代理对象操作全局变量
- legacySandbox.proxy.customVar = 'legacy-value'
- legacySandbox.proxy.existingVar = 'modified-value'
- // 失活沙箱
- legacySandbox.inactive()
- console.log(window.customVar) // undefined
- console.log(window.existingVar) // 恢复原始值
JavaScript复制
遗留沙箱的特点
优点:
- 性能比快照沙箱好,只记录变化的属性
- 使用 Proxy 可以精确拦截属性操作
- 支持属性的增删改查操作
缺点:
- 仍然直接操作 window 对象,
无法支持多实例
- 需要浏览器支持 Proxy(IE11+ 及现代浏览器)
3. ProxySandbox 代理沙箱
代理沙箱是乾坤最新的沙箱实现,为每个微应用创建一个独立的代理对象
,真正实现了多实例隔离。
- /**
- * 代理沙箱实现
- */
- class ProxySandbox {
- constructor(name) {
- this.name = name
- this.active = false
-
- // 沙箱的私有变量存储
- this.fakeWindow = {}
-
- // 记录哪些属性是沙箱私有的
- this.propertiesWithGetter = new Map()
-
- // 创建代理对象
- this.proxy = new Proxy(this.fakeWindow, {
- get: (target, prop) => {
- // 如果沙箱处于激活状态
- if (this.active) {
- // 优先从沙箱私有变量中获取
- if (prop in target) {
- return target[prop]
- }
-
- // 如果沙箱中没有,则从 window 获取
- const value = window[prop]
-
- // 如果是函数,需要绑定正确的 this
- if (typeof value === 'function') {
- // 对于 window 上的函数,绑定 window 作为 this
- const boundValue = value.bind(window)
-
- // 缓存绑定后的函数,避免重复绑定
- target[prop] = boundValue
- return boundValue
- }
-
- return value
- }
-
- // 沙箱未激活时,直接返回 window 上的值
- return window[prop]
- },
-
- set: (target, prop, value) => {
- if (this.active) {
- // 检查是否是 window 的原生属性
- if (this.isWindowProperty(prop)) {
- // 如果是 window 的原生属性,直接设置到 window
- window[prop] = value
- } else {
- // 否则设置到沙箱的私有空间
- target[prop] = value
- }
- } else {
- // 沙箱未激活时,直接设置到 window
- window[prop] = value
- }
-
- return true
- },
-
- deleteProperty: (target, prop) => {
- if (this.active) {
- if (prop in target) {
- delete target[prop]
- } else if (prop in window) {
- delete window[prop]
- }
- } else {
- delete window[prop]
- }
-
- return true
- },
-
- has: (target, prop) => {
- return prop in target || prop in window
- },
-
- ownKeys: (target) => {
- return [...Object.getOwnPropertyNames(target), ...Object.getOwnPropertyNames(window)]
- },
-
- getOwnPropertyDescriptor: (target, prop) => {
- if (prop in target) {
- return Object.getOwnPropertyDescriptor(target, prop)
- }
-
- if (prop in window) {
- return Object.getOwnPropertyDescriptor(window, prop)
- }
-
- return undefined
- }
- })
- }
- /**
- * 判断属性是否为 window 的原生属性
- */
- isWindowProperty(prop) {
- // 这里可以根据具体需求判断
- // 例如:document, location, navigator 等应该直接操作 window
- const windowProperties = [
- 'document', 'location', 'navigator', 'history', 'screen',
- 'alert', 'confirm', 'prompt', 'setTimeout', 'clearTimeout',
- 'setInterval', 'clearInterval', 'requestAnimationFrame',
- 'cancelAnimationFrame', 'fetch', 'XMLHttpRequest'
- ]
-
- return windowProperties.includes(prop) ||
- prop.startsWith('webkit') ||
- prop.startsWith('moz') ||
- prop.startsWith('ms')
- }
- /**
- * 激活沙箱
- */
- active() {
- this.active = true
- }
- /**
- * 失活沙箱
- */
- inactive() {
- this.active = false
- }
-
- /**
- * 获取沙箱的全局代理对象
- */
- getProxy() {
- return this.proxy
- }
- }
- // 使用示例
- const proxySandbox1 = new ProxySandbox('app1')
- const proxySandbox2 = new ProxySandbox('app2')
- // 激活两个沙箱
- proxySandbox1.active()
- proxySandbox2.active()
- // 在不同沙箱中设置同名变量
- const proxy1 = proxySandbox1.getProxy()
- const proxy2 = proxySandbox2.getProxy()
- proxy1.customVar = 'value-from-app1'
- proxy2.customVar = 'value-from-app2'
- console.log(proxy1.customVar) // 'value-from-app1'
- console.log(proxy2.customVar) // 'value-from-app2'
- console.log(window.customVar) // undefined
- // 访问 window 原生属性
- console.log(proxy1.document === window.document) // true
- console.log(proxy2.location === window.location) // true
JavaScript复制
代理沙箱的特点
优点:
- 真正实现了多实例隔离,多个微应用可以并行运行
- 性能最佳,每个沙箱只维护自己的变量
- 提供了最完整的 JavaScript 沙箱能力
缺点:
- 实现复杂度最高
- 需要浏览器支持 Proxy 和 Reflect
乾坤沙箱源码学习
沙箱工厂函数
乾坤根据浏览器能力和配置自动选择合适的沙箱实现:
- /**
- * 乾坤沙箱创建工厂
- */
- function createSandbox(appName, elementGetter, scopedCSS, useLooseSandbox) {
- let sandbox
-
- // 检查浏览器对 Proxy 的支持
- if (window.Proxy) {
- // 如果需要严格沙箱且支持 Proxy,使用 ProxySandbox
- if (!useLooseSandbox) {
- sandbox = new ProxySandbox(appName)
- } else {
- // 否则使用 LegacySandbox(向后兼容)
- sandbox = new LegacySandbox(appName)
- }
- } else {
- // 不支持 Proxy 的情况下使用 SnapshotSandbox
- sandbox = new SnapshotSandbox(appName)
- }
-
- return sandbox
- }
- /**
- * 沙箱管理器
- */
- class SandboxManager {
- constructor() {
- this.sandboxes = new Map()
- }
-
- /**
- * 创建沙箱
- */
- createSandbox(appName, options = {}) {
- if (this.sandboxes.has(appName)) {
- return this.sandboxes.get(appName)
- }
-
- const sandbox = createSandbox(
- appName,
- options.elementGetter,
- options.scopedCSS,
- options.useLooseSandbox
- )
-
- this.sandboxes.set(appName, sandbox)
- return sandbox
- }
-
- /**
- * 激活沙箱
- */
- activateSandbox(appName) {
- const sandbox = this.sandboxes.get(appName)
- if (sandbox) {
- sandbox.active()
- return sandbox.proxy || window
- }
- return window
- }
-
- /**
- * 失活沙箱
- */
- deactivateSandbox(appName) {
- const sandbox = this.sandboxes.get(appName)
- if (sandbox) {
- sandbox.inactive()
- }
- }
-
- /**
- * 销毁沙箱
- */
- destroySandbox(appName) {
- const sandbox = this.sandboxes.get(appName)
- if (sandbox) {
- sandbox.inactive()
- this.sandboxes.delete(appName)
- }
- }
- }
- // 乾坤全局沙箱管理器
- const sandboxManager = new SandboxManager()
- export { sandboxManager, createSandbox }
JavaScript复制
沙箱与微应用生命周期整合
- /**
- * 微应用生命周期管理
- */
- class MicroAppLifecycle {
- constructor(appName, options) {
- this.appName = appName
- this.options = options
- this.sandbox = null
- this.globalProxy = null
- }
-
- /**
- * 微应用挂载前
- */
- async beforeMount() {
- // 创建沙箱
- this.sandbox = sandboxManager.createSandbox(this.appName, this.options)
-
- // 激活沙箱并获取全局代理对象
- this.globalProxy = sandboxManager.activateSandbox(this.appName)
-
- console.log(`${this.appName} 沙箱已激活`)
- }
-
- /**
- * 微应用挂载
- */
- async mount() {
- // 在沙箱环境中执行微应用的挂载逻辑
- const originalWindow = window
-
- try {
- // 临时替换全局 window 对象
- if (this.globalProxy && this.globalProxy !== window) {
- // 这里需要通过特殊方式注入 globalProxy
- // 实际实现会更复杂,涉及到模块加载器的改造
- }
-
- // 执行微应用挂载逻辑
- await this.executeAppMount()
-
- } finally {
- // 恢复原始 window 对象
- // window = originalWindow
- }
- }
-
- /**
- * 微应用卸载
- */
- async unmount() {
- // 执行微应用卸载逻辑
- await this.executeAppUnmount()
-
- // 失活沙箱
- sandboxManager.deactivateSandbox(this.appName)
-
- console.log(`${this.appName} 沙箱已失活`)
- }
-
- /**
- * 微应用销毁
- */
- async destroy() {
- // 销毁沙箱
- sandboxManager.destroySandbox(this.appName)
-
- console.log(`${this.appName} 沙箱已销毁`)
- }
-
- /**
- * 执行微应用挂载(需要在沙箱环境中)
- */
- async executeAppMount() {
- // 这里会加载并执行微应用的 JavaScript 代码
- // 实际实现涉及到模块加载、代码执行等复杂逻辑
- }
-
- /**
- * 执行微应用卸载
- */
- async executeAppUnmount() {
- // 这里会执行微应用的清理逻辑
- }
- }
- // 使用示例
- const microApp = new MicroAppLifecycle('my-micro-app', {
- useLooseSandbox: false,
- scopedCSS: true
- })
- // 微应用生命周期调用
- async function runMicroApp() {
- await microApp.beforeMount()
- await microApp.mount()
-
- // 模拟应用运行一段时间
- setTimeout(async () => {
- await microApp.unmount()
- await microApp.destroy()
- }, 5000)
- }
JavaScript复制
沙箱的高级特性
1. 副作用清理
乾坤沙箱不仅隔离全局变量,还会自动清理微应用产生的副作用:
- /**
- * 副作用收集器
- */
- class SideEffectCollector {
- constructor() {
- this.effects = []
- }
-
- /**
- * 收集定时器副作用
- */
- patchTimer() {
- const originalSetTimeout = window.setTimeout
- const originalSetInterval = window.setInterval
- const originalClearTimeout = window.clearTimeout
- const originalClearInterval = window.clearInterval
-
- const timers = new Set()
-
- // 代理 setTimeout
- window.setTimeout = (callback, delay, ...args) => {
- const timer = originalSetTimeout(callback, delay, ...args)
- timers.add(timer)
- return timer
- }
-
- // 代理 setInterval
- window.setInterval = (callback, delay, ...args) => {
- const timer = originalSetInterval(callback, delay, ...args)
- timers.add(timer)
- return timer
- }
-
- // 代理 clearTimeout
- window.clearTimeout = (timer) => {
- timers.delete(timer)
- return originalClearTimeout(timer)
- }
-
- // 代理 clearInterval
- window.clearInterval = (timer) => {
- timers.delete(timer)
- return originalClearInterval(timer)
- }
-
- // 记录恢复函数
- this.effects.push(() => {
- // 清理所有未清理的定时器
- timers.forEach(timer => {
- originalClearTimeout(timer)
- originalClearInterval(timer)
- })
-
- // 恢复原始函数
- window.setTimeout = originalSetTimeout
- window.setInterval = originalSetInterval
- window.clearTimeout = originalClearTimeout
- window.clearInterval = originalClearInterval
- })
- }
-
- /**
- * 收集事件监听器副作用
- */
- patchEventListener() {
- const originalAddEventListener = window.addEventListener
- const originalRemoveEventListener = window.removeEventListener
-
- const listeners = new Map()
-
- // 代理 addEventListener
- window.addEventListener = (type, listener, options) => {
- const key = { type, listener, options }
- listeners.set(key, { type, listener, options })
- return originalAddEventListener.call(window, type, listener, options)
- }
-
- // 代理 removeEventListener
- window.removeEventListener = (type, listener, options) => {
- const key = Array.from(listeners.keys()).find(k =>
- k.type === type && k.listener === listener && k.options === options
- )
- if (key) {
- listeners.delete(key)
- }
- return originalRemoveEventListener.call(window, type, listener, options)
- }
-
- // 记录恢复函数
- this.effects.push(() => {
- // 清理所有未清理的事件监听器
- listeners.forEach(({ type, listener, options }) => {
- originalRemoveEventListener.call(window, type, listener, options)
- })
-
- // 恢复原始函数
- window.addEventListener = originalAddEventListener
- window.removeEventListener = originalRemoveEventListener
- })
- }
-
- /**
- * 收集 DOM 变更副作用
- */
- patchDOMChanges() {
- const addedNodes = new Set()
- const modifiedAttributes = new Map()
-
- // 监控 DOM 变更
- const observer = new MutationObserver(mutations => {
- mutations.forEach(mutation => {
- if (mutation.type === 'childList') {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- addedNodes.add(node)
- }
- })
- } else if (mutation.type === 'attributes') {
- if (!modifiedAttributes.has(mutation.target)) {
- modifiedAttributes.set(mutation.target, new Map())
- }
- modifiedAttributes.get(mutation.target).set(
- mutation.attributeName,
- mutation.oldValue
- )
- }
- })
- })
-
- observer.observe(document, {
- childList: true,
- subtree: true,
- attributes: true,
- attributeOldValue: true
- })
-
- // 记录恢复函数
- this.effects.push(() => {
- // 停止监控
- observer.disconnect()
-
- // 清理新增的 DOM 节点
- addedNodes.forEach(node => {
- if (node.parentNode) {
- node.parentNode.removeChild(node)
- }
- })
-
- // 恢复被修改的属性
- modifiedAttributes.forEach((attrs, element) => {
- attrs.forEach((oldValue, attrName) => {
- if (oldValue === null) {
- element.removeAttribute(attrName)
- } else {
- element.setAttribute(attrName, oldValue)
- }
- })
- })
- })
- }
-
- /**
- * 应用所有补丁
- */
- applyPatches() {
- this.patchTimer()
- this.patchEventListener()
- this.patchDOMChanges()
- }
-
- /**
- * 清理所有副作用
- */
- cleanup() {
- this.effects.forEach(effect => effect())
- this.effects = []
- }
- }
- /**
- * 增强的代理沙箱,集成副作用管理
- */
- class EnhancedProxySandbox extends ProxySandbox {
- constructor(name) {
- super(name)
- this.sideEffectCollector = new SideEffectCollector()
- }
-
- active() {
- super.active()
- // 应用副作用收集补丁
- this.sideEffectCollector.applyPatches()
- }
-
- inactive() {
- // 清理副作用
- this.sideEffectCollector.cleanup()
- super.inactive()
- }
- }
JavaScript复制
2. CSS 隔离
乾坤也提供了 CSS 隔离机制:
- /**
- * CSS 沙箱实现
- */
- class CSSSandbox {
- constructor(appName, container) {
- this.appName = appName
- this.container = container
- this.dynamicStyleSheets = new Set()
- this.modifiedStyles = new Map()
- }
-
- /**
- * 激活 CSS 沙箱
- */
- activate() {
- this.patchDynamicCSS()
- this.scopeStaticCSS()
- }
-
- /**
- * 处理动态添加的样式
- */
- patchDynamicCSS() {
- const originalCreateElement = document.createElement
- const originalAppendChild = Node.prototype.appendChild
- const originalInsertBefore = Node.prototype.insertBefore
-
- // 代理 createElement
- document.createElement = (tagName) => {
- const element = originalCreateElement.call(document, tagName)
-
- if (tagName.toLowerCase() === 'style') {
- this.dynamicStyleSheets.add(element)
- }
-
- return element
- }
-
- // 代理 appendChild
- Node.prototype.appendChild = function(child) {
- if (child.tagName === 'STYLE' || child.tagName === 'LINK') {
- this.dynamicStyleSheets.add(child)
-
- // 为动态样式添加作用域
- this.scopeStyleSheet(child)
- }
-
- return originalAppendChild.call(this, child)
- }
-
- // 记录原始函数用于恢复
- this.restoreFunctions = () => {
- document.createElement = originalCreateElement
- Node.prototype.appendChild = originalAppendChild
- Node.prototype.insertBefore = originalInsertBefore
- }
- }
-
- /**
- * 为样式表添加作用域
- */
- scopeStyleSheet(styleElement) {
- // 添加特殊的属性前缀,防止样式冲突
- const appSelector = `[data-qiankun="${this.appName}"]`
-
- if (styleElement.sheet) {
- const rules = Array.from(styleElement.sheet.cssRules || styleElement.sheet.rules)
-
- rules.forEach((rule, index) => {
- if (rule.type === CSSRule.STYLE_RULE) {
- const originalSelector = rule.selectorText
- const scopedSelector = `${appSelector} ${originalSelector}`
-
- // 删除原规则并添加带作用域的新规则
- styleElement.sheet.deleteRule(index)
- styleElement.sheet.insertRule(`${scopedSelector} { ${rule.style.cssText} }`, index)
- }
- })
- }
- }
-
- /**
- * 处理静态 CSS
- */
- scopeStaticCSS() {
- const styleSheets = Array.from(document.styleSheets)
-
- styleSheets.forEach(sheet => {
- try {
- if (sheet.ownerNode && this.isAppStyleSheet(sheet.ownerNode)) {
- this.scopeStyleSheet(sheet.ownerNode)
- }
- } catch (e) {
- // 跨域样式表无法访问
- console.warn('无法处理跨域样式表:', e)
- }
- })
- }
-
- /**
- * 判断是否为当前应用的样式表
- */
- isAppStyleSheet(element) {
- // 根据元素位置或其他标识判断是否为当前应用的样式
- return this.container.contains(element)
- }
-
- /**
- * 失活 CSS 沙箱
- */
- deactivate() {
- // 恢复原始函数
- if (this.restoreFunctions) {
- this.restoreFunctions()
- }
-
- // 移除动态添加的样式
- this.dynamicStyleSheets.forEach(element => {
- if (element.parentNode) {
- element.parentNode.removeChild(element)
- }
- })
-
- this.dynamicStyleSheets.clear()
- }
- }
JavaScript复制
实际应用场景
1. 电商平台微前端架构
- /**
- * 电商平台微前端沙箱配置
- */
- class ECommerceSandboxConfig {
- static getConfig(appName) {
- const configs = {
- // 主应用 - 不需要沙箱
- 'main-app': {
- sandbox: false
- },
-
- // 用户中心 - 需要严格隔离
- 'user-center': {
- sandbox: true,
- useLooseSandbox: false,
- scopedCSS: true,
- customProps: {
- // 共享用户信息
- userInfo: window.globalUserInfo,
- // 共享通用工具
- utils: window.commonUtils
- }
- },
-
- // 商品模块 - 允许宽松沙箱以提升性能
- 'product-module': {
- sandbox: true,
- useLooseSandbox: true,
- scopedCSS: true,
- customProps: {
- productAPI: window.productAPI
- }
- },
-
- // 支付模块 - 最严格的隔离
- 'payment-module': {
- sandbox: true,
- useLooseSandbox: false,
- scopedCSS: true,
- singular: true, // 单例模式
- customProps: {
- paymentConfig: window.securePaymentConfig
- }
- }
- }
-
- return configs[appName] || { sandbox: true }
- }
- }
- /**
- * 微前端应用管理器
- */
- class MicroAppManager {
- constructor() {
- this.apps = new Map()
- this.sandboxManager = new SandboxManager()
- }
-
- /**
- * 注册微应用
- */
- registerApp(appConfig) {
- const { name, entry, container, activeRule } = appConfig
- const sandboxConfig = ECommerceSandboxConfig.getConfig(name)
-
- this.apps.set(name, {
- ...appConfig,
- sandboxConfig,
- status: 'registered'
- })
- }
-
- /**
- * 加载并挂载应用
- */
- async mountApp(appName) {
- const appConfig = this.apps.get(appName)
- if (!appConfig) {
- throw new Error(`应用 ${appName} 未注册`)
- }
-
- try {
- // 创建沙箱
- if (appConfig.sandboxConfig.sandbox) {
- const sandbox = this.sandboxManager.createSandbox(appName, appConfig.sandboxConfig)
- const globalProxy = this.sandboxManager.activateSandbox(appName)
-
- // 注入自定义属性
- if (appConfig.sandboxConfig.customProps) {
- Object.keys(appConfig.sandboxConfig.customProps).forEach(key => {
- globalProxy[key] = appConfig.sandboxConfig.customProps[key]
- })
- }
- }
-
- // 加载应用资源
- const { template, execScripts } = await this.loadApp(appConfig.entry)
-
- // 渲染应用模板
- const container = document.querySelector(appConfig.container)
- container.innerHTML = template
-
- // 在沙箱环境中执行应用脚本
- const appExports = await execScripts(
- appConfig.sandboxConfig.sandbox ?
- this.sandboxManager.sandboxes.get(appName).proxy :
- window
- )
-
- // 执行应用挂载
- if (appExports.mount) {
- await appExports.mount({
- container,
- props: appConfig.sandboxConfig.customProps || {}
- })
- }
-
- appConfig.status = 'mounted'
- appConfig.appExports = appExports
-
- } catch (error) {
- console.error(`挂载应用 ${appName} 失败:`, error)
- appConfig.status = 'load_error'
- }
- }
-
- /**
- * 卸载应用
- */
- async unmountApp(appName) {
- const appConfig = this.apps.get(appName)
- if (!appConfig || appConfig.status !== 'mounted') {
- return
- }
-
- try {
- // 执行应用卸载
- if (appConfig.appExports && appConfig.appExports.unmount) {
- await appConfig.appExports.unmount()
- }
-
- // 失活沙箱
- if (appConfig.sandboxConfig.sandbox) {
- this.sandboxManager.deactivateSandbox(appName)
- }
-
- // 清理容器
- const container = document.querySelector(appConfig.container)
- container.innerHTML = ''
-
- appConfig.status = 'unmounted'
-
- } catch (error) {
- console.error(`卸载应用 ${appName} 失败:`, error)
- }
- }
-
- /**
- * 加载应用资源
- */
- async loadApp(entry) {
- // 这里实现应用资源加载逻辑
- // 包括 HTML、CSS、JavaScript 的获取和处理
-
- const response = await fetch(entry)
- const html = await response.text()
-
- // 解析 HTML,提取 CSS 和 JS
- const { template, scripts, styles } = this.parseHTML(html)
-
- // 创建脚本执行函数
- const execScripts = (global = window) => {
- return new Promise((resolve) => {
- const exports = {}
-
- scripts.forEach(script => {
- // 在指定的全局环境中执行脚本
- const func = new Function('window', 'exports', script)
- func.call(global, global, exports)
- })
-
- resolve(exports)
- })
- }
-
- return { template, execScripts }
- }
-
- /**
- * 解析 HTML 内容
- */
- parseHTML(html) {
- // HTML 解析逻辑
- const template = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
-
- const scripts = []
- const scriptRegex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi
-
- let match
- while ((match = scriptRegex.exec(html)) !== null) {
- const scriptContent = match[0].replace(/<script[^>]*>|<\/script>/gi, '')
- scripts.push(scriptContent)
- }
-
- return { template, scripts, styles: [] }
- }
- }
- // 使用示例
- const appManager = new MicroAppManager()
- // 注册微应用
- appManager.registerApp({
- name: 'user-center',
- entry: 'http://localhost:3001',
- container: '#user-center-container',
- activeRule: '/user'
- })
- appManager.registerApp({
- name: 'product-module',
- entry: 'http://localhost:3002',
- container: '#product-container',
- activeRule: '/product'
- })
- // 路由变化时挂载/卸载应用
- window.addEventListener('popstate', () => {
- const pathname = window.location.pathname
-
- if (pathname.startsWith('/user')) {
- appManager.mountApp('user-center')
- } else if (pathname.startsWith('/product')) {
- appManager.mountApp('product-module')
- }
- })
JavaScript复制
2. 开发环境沙箱调试
- /**
- * 沙箱调试工具
- */
- class SandboxDebugger {
- constructor(sandbox) {
- this.sandbox = sandbox
- this.logs = []
- this.startTime = Date.now()
- }
-
- /**
- * 开始监控沙箱
- */
- startMonitoring() {
- this.monitorProxyAccess()
- this.monitorSideEffects()
- this.monitorPerformance()
- }
-
- /**
- * 监控代理访问
- */
- monitorProxyAccess() {
- const originalProxy = this.sandbox.proxy
-
- this.sandbox.proxy = new Proxy(originalProxy, {
- get: (target, prop, receiver) => {
- this.log('GET', prop, Reflect.get(target, prop, receiver))
- return Reflect.get(target, prop, receiver)
- },
-
- set: (target, prop, value, receiver) => {
- this.log('SET', prop, value)
- return Reflect.set(target, prop, value, receiver)
- },
-
- deleteProperty: (target, prop) => {
- this.log('DELETE', prop)
- return Reflect.deleteProperty(target, prop)
- }
- })
- }
-
- /**
- * 监控副作用
- */
- monitorSideEffects() {
- // 监控定时器
- const originalSetTimeout = window.setTimeout
- window.setTimeout = (...args) => {
- this.log('SIDE_EFFECT', 'setTimeout', args[1] || 0)
- return originalSetTimeout.apply(window, args)
- }
-
- // 监控事件监听器
- const originalAddEventListener = window.addEventListener
- window.addEventListener = (...args) => {
- this.log('SIDE_EFFECT', 'addEventListener', args[0])
- return originalAddEventListener.apply(window, args)
- }
- }
-
- /**
- * 监控性能
- */
- monitorPerformance() {
- const observer = new PerformanceObserver(list => {
- list.getEntries().forEach(entry => {
- if (entry.entryType === 'measure' || entry.entryType === 'navigation') {
- this.log('PERFORMANCE', entry.name, entry.duration)
- }
- })
- })
-
- observer.observe({ entryTypes: ['measure', 'navigation'] })
- }
-
- /**
- * 记录日志
- */
- log(type, property, value) {
- const logEntry = {
- timestamp: Date.now() - this.startTime,
- type,
- property,
- value,
- stack: new Error().stack
- }
-
- this.logs.push(logEntry)
-
- // 开发环境下打印日志
- if (process.env.NODE_ENV === 'development') {
- console.log(`[沙箱调试] ${type}: ${property}`, value)
- }
- }
-
- /**
- * 生成调试报告
- */
- generateReport() {
- const report = {
- sandboxName: this.sandbox.name,
- duration: Date.now() - this.startTime,
- totalOperations: this.logs.length,
- operationsByType: {},
- performanceMetrics: {},
- sideEffects: []
- }
-
- // 统计操作类型
- this.logs.forEach(log => {
- report.operationsByType[log.type] = (report.operationsByType[log.type] || 0) + 1
-
- if (log.type === 'SIDE_EFFECT') {
- report.sideEffects.push(log)
- } else if (log.type === 'PERFORMANCE') {
- report.performanceMetrics[log.property] = log.value
- }
- })
-
- return report
- }
-
- /**
- * 导出调试数据
- */
- exportDebugData() {
- const data = {
- report: this.generateReport(),
- logs: this.logs
- }
-
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
- const url = URL.createObjectURL(blob)
-
- const a = document.createElement('a')
- a.href = url
- a.download = `sandbox-debug-${this.sandbox.name}-${Date.now()}.json`
- a.click()
-
- URL.revokeObjectURL(url)
- }
- }
- // 使用示例
- const sandbox = new ProxySandbox('debug-app')
- const debugger = new SandboxDebugger(sandbox)
- sandbox.active()
- debugger.startMonitoring()
- // 运行一段时间后生成报告
- setTimeout(() => {
- const report = debugger.generateReport()
- console.log('沙箱调试报告:', report)
-
- // 导出详细调试数据
- debugger.exportDebugData()
- }, 10000)
JavaScript复制
性能优化与最佳实践
1. 沙箱性能优化
- /**
- * 性能优化的沙箱实现
- */
- class OptimizedProxySandbox extends ProxySandbox {
- constructor(name, options = {}) {
- super(name)
-
- this.options = {
- // 是否启用属性缓存
- enablePropertyCache: true,
- // 缓存大小限制
- maxCacheSize: 1000,
- // 是否启用懒加载
- enableLazyLoading: true,
- ...options
- }
-
- // 属性访问缓存
- this.propertyCache = new Map()
-
- // 热点属性(频繁访问的属性)
- this.hotProperties = new Set([
- 'document', 'location', 'navigator', 'console',
- 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'
- ])
-
- this.optimizeProxy()
- }
-
- /**
- * 优化代理对象
- */
- optimizeProxy() {
- const originalProxy = this.proxy
-
- this.proxy = new Proxy(this.fakeWindow, {
- get: (target, prop) => {
- // 对于热点属性,直接返回不走复杂逻辑
- if (this.hotProperties.has(prop)) {
- return window[prop]
- }
-
- // 使用缓存
- if (this.options.enablePropertyCache && this.propertyCache.has(prop)) {
- return this.propertyCache.get(prop)
- }
-
- const value = originalProxy[prop]
-
- // 缓存结果
- if (this.options.enablePropertyCache && this.propertyCache.size < this.options.maxCacheSize) {
- this.propertyCache.set(prop, value)
- }
-
- return value
- },
-
- set: (target, prop, value) => {
- // 清除相关缓存
- if (this.options.enablePropertyCache) {
- this.propertyCache.delete(prop)
- }
-
- return Reflect.set(originalProxy, prop, value)
- }
- })
- }
-
- /**
- * 预热常用属性
- */
- preheatProperties() {
- const commonProperties = [
- 'document', 'location', 'navigator', 'console',
- 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval',
- 'requestAnimationFrame', 'cancelAnimationFrame',
- 'fetch', 'XMLHttpRequest', 'localStorage', 'sessionStorage'
- ]
-
- commonProperties.forEach(prop => {
- // 触发属性访问以填充缓存
- this.proxy[prop]
- })
- }
-
- /**
- * 清理缓存
- */
- clearCache() {
- this.propertyCache.clear()
- }
-
- active() {
- super.active()
-
- // 预热属性
- if (this.options.enablePropertyCache) {
- this.preheatProperties()
- }
- }
-
- inactive() {
- // 清理缓存
- this.clearCache()
- super.inactive()
- }
- }
JavaScript复制
2. 沙箱使用最佳实践
- /**
- * 沙箱最佳实践指南
- */
- class SandboxBestPractices {
- /**
- * 创建生产环境推荐的沙箱配置
- */
- static createProductionConfig(appName, options = {}) {
- return {
- // 根据浏览器支持情况自动选择沙箱类型
- sandbox: 'auto', // auto | strict | loose | false
-
- // CSS 隔离
- scopedCSS: true,
-
- // 预加载优化
- prefetch: true,
-
- // 资源缓存
- cache: true,
-
- // 性能监控
- enablePerformanceMonitor: true,
-
- // 自定义沙箱配置
- sandboxConfig: {
- enablePropertyCache: true,
- maxCacheSize: 500,
- enableLazyLoading: true
- },
-
- // 全局变量白名单(允许访问的全局变量)
- globalWhitelist: [
- 'document',
- 'location',
- 'navigator',
- 'console',
- // 第三方库
- 'React',
- 'Vue',
- 'jQuery',
- // 业务全局变量
- 'APP_CONFIG',
- 'USER_INFO'
- ],
-
- // 副作用清理配置
- sideEffectConfig: {
- // 自动清理定时器
- autoCleanupTimers: true,
- // 自动清理事件监听器
- autoCleanupListeners: true,
- // 自动清理 DOM 变更
- autoCleanupDOM: true,
- // 自动清理网络请求
- autoCleanupRequests: false
- },
-
- ...options
- }
- }
-
- /**
- * 沙箱错误处理
- */
- static createErrorHandler(appName) {
- return {
- onLoadError: (error) => {
- console.error(`[${appName}] 应用加载失败:`, error)
-
- // 上报错误
- this.reportError('load_error', { appName, error: error.message })
- },
-
- onMountError: (error) => {
- console.error(`[${appName}] 应用挂载失败:`, error)
-
- // 尝试降级处理
- this.fallbackMount(appName)
- },
-
- onUnmountError: (error) => {
- console.error(`[${appName}] 应用卸载失败:`, error)
-
- // 强制清理
- this.forceCleanup(appName)
- },
-
- onSandboxError: (error) => {
- console.error(`[${appName}] 沙箱错误:`, error)
-
- // 重建沙箱
- this.recreateSandbox(appName)
- }
- }
- }
-
- /**
- * 上报错误
- */
- static reportError(type, data) {
- // 发送错误信息到监控系统
- fetch('/api/error-report', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- type,
- data,
- timestamp: Date.now(),
- userAgent: navigator.userAgent,
- url: window.location.href
- })
- }).catch(console.error)
- }
-
- /**
- * 降级挂载
- */
- static fallbackMount(appName) {
- console.log(`[${appName}] 尝试降级挂载`)
-
- // 使用更宽松的沙箱配置重试
- const fallbackConfig = {
- sandbox: 'loose',
- scopedCSS: false
- }
-
- // 重新挂载应用
- // ... 实现降级逻辑
- }
-
- /**
- * 强制清理
- */
- static forceCleanup(appName) {
- console.log(`[${appName}] 执行强制清理`)
-
- // 清理容器
- const containers = document.querySelectorAll(`[data-app="${appName}"]`)
- containers.forEach(container => {
- container.innerHTML = ''
- })
-
- // 清理全局状态
- delete window[`__MICRO_APP_${appName.toUpperCase()}__`]
- }
-
- /**
- * 重建沙箱
- */
- static recreateSandbox(appName) {
- console.log(`[${appName}] 重建沙箱`)
-
- // 销毁现有沙箱
- sandboxManager.destroySandbox(appName)
-
- // 创建新沙箱
- const config = this.createProductionConfig(appName)
- sandboxManager.createSandbox(appName, config.sandboxConfig)
- }
- }
- /**
- * 应用性能监控
- */
- class AppPerformanceMonitor {
- constructor(appName) {
- this.appName = appName
- this.metrics = {
- loadTime: 0,
- mountTime: 0,
- unmountTime: 0,
- memoryUsage: 0,
- errorCount: 0
- }
- }
-
- /**
- * 开始监控应用加载
- */
- startLoadMonitoring() {
- this.loadStartTime = performance.now()
- }
-
- /**
- * 结束加载监控
- */
- endLoadMonitoring() {
- this.metrics.loadTime = performance.now() - this.loadStartTime
- }
-
- /**
- * 开始挂载监控
- */
- startMountMonitoring() {
- this.mountStartTime = performance.now()
- }
-
- /**
- * 结束挂载监控
- */
- endMountMonitoring() {
- this.metrics.mountTime = performance.now() - this.mountStartTime
- }
-
- /**
- * 监控内存使用
- */
- monitorMemoryUsage() {
- if ('memory' in performance) {
- this.metrics.memoryUsage = performance.memory.usedJSHeapSize
- }
- }
-
- /**
- * 记录错误
- */
- recordError(error) {
- this.metrics.errorCount++
-
- // 上报性能数据
- this.reportMetrics()
- }
-
- /**
- * 上报性能指标
- */
- reportMetrics() {
- const data = {
- appName: this.appName,
- metrics: this.metrics,
- timestamp: Date.now()
- }
-
- console.log(`[性能监控] ${this.appName}:`, data)
-
- // 发送到监控系统
- fetch('/api/performance-metrics', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- }).catch(console.error)
- }
- }
JavaScript复制
总结
乾坤的沙箱机制是微前端架构中的核心技术,它解决了多个独立应用在同一页面中运行时的隔离问题。
通过三种不同的沙箱实现,乾坤能够在不同的浏览器环境中提供最佳的隔离效果:
关键特性:
- 渐进式隔离:根据浏览器能力自动选择合适的沙箱实现
- 多实例支持:ProxySandbox 支持多个微应用同时运行
- 副作用管理:自动清理定时器、事件监听器等副作用
- 性能优化:通过缓存、预热等技术提升沙箱性能
- 开发友好:提供调试工具和错误处理机制
使用建议:
- 生产环境:优先使用 ProxySandbox,启用完整的隔离和副作用管理
- 开发环境:可以使用调试模式,便于排查问题
- 性能敏感场景:可以考虑使用 LegacySandbox 或关闭部分隔离特性
- 兼容性要求高:使用 SnapshotSandbox 以支持旧版浏览器