# Qiankun 原理-js沙箱是怎么做隔离的

107

嗨喽,大家好,我是前端明明,最近这几天在学习乾坤微前端在这里分享下乾坤做js隔离的三种方式。它们分别是SnapshotSandbox,LegacySandbox ,ProxySandbox 等沙箱。

什么是沙箱
    沙箱,即sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。

JS 沙箱,确保微应用之间 全局变量/事件 不冲突。

1. 为啥要有js沙箱

假如说我们在子应用A上的window 定义了一个变量window.a =3, 在子应用B上又定义了一个变量 window.b =4 , 当我们A ,B应用切换时那么window.a 的值怎么处理。正确的肯定是在A应用上window.a=3 ,切到b 应用上window.b = 4 。实现这个就需要用到沙箱。实现沙箱乾坤用了以下三种方式,各有优略点。

2. SnapshotSandbox 快照沙箱

顾名思义,快照沙箱就是,给你拍一张照片,记录你此时的状态,乾坤的快照沙箱是根据diff 来实现的,主要用于不支持window.Proxy的低版本浏览器,而且只是使用雨单个的自用用

快照沙箱的原理

激活沙箱时,将window的快照信息存在windowSnapshot中,如果modifyPropsMap有值,还需要还原上次的状态,激活期间,可能修改了window的数据,退出沙箱,将修改过的信息存到modifyPropsMap 里面,并且吧window ,还原初始进入的状态

class Sanapshotbox {
    // 激活
    windowSnapshot = {}
    modifyPropsMap = {}
​
    
    active() {
        // 保存window 对象上所有属性的状态
        for (const prop in window) {
            this.windowSnapshot[prop] = window[prop]
        }
        // 恢复上一次在运行微应用的时候改过的window 上的属性
        Object.keys(this.modifyPropsMap).forEach(prop => {
            window[prop] = this.modifyPropsMap[prop]
        })
    }
    // 失活 
    inactive() {
        // 记录修改window 上的哪些属性
        for (const prop in window) {
            if (window[prop] !== this.windowSnapshot[prop]) {
                this.modifyPropsMap[prop] = window[prop]
                // 将window 上的属性状态 还原至微应用之前的状态 
                window[prop] = this.windowSnapshot[prop]
            }
        }
    }
}
window.city="beijing"  // 初始值
let sanapshotbox  = new Sanapshotbox()
console.log("111",window.city)
sanapshotbox.active()  // 微应用运行
window.city="上海"
console.log("22222",window.city)
sanapshotbox.inactive()  // 微应用卸载了
console.log('3333',window.city)
​

缺点

  1. 需要遍历window上的所有属性,性能差
  2. 同时间内只能激活一个微应用

代理沙箱

Qiankun 基于es6 的Proxy 实现了两种应用场景不同的沙箱,一种是legacySandbox(单例),一种是proxySandbox(多例)。因为都是基于Proxy实现的,所以称为代理沙箱。

legacySandbox实现原理

legacySandbox 设置三个参数来记录全局变量,分别记录沙箱新增的全局变量addedPropsMapInSandbox,记录沙箱更新的全局变量modifiedPropsOriginalValueMapInSandbox,用于在任意时刻做snapshot的currentUpdatePropsValueMap。

legacySandbox demo

class LegacySandbox {
    // 持续记录新增和修改的全局变量
    currentUpdatePropsValueMap = new Map()
    // 沙箱期间更新的全局变量
    modifiedPropsOriginalValueMapInSandbox = new Map()
    // 沙箱期间新增的全局变量
    addedPropsMapInSandbox = new Map()
    propsWindow = {}
​
    // 核心逻辑
    constructor() {
        const fakeWindow = Object.create(null)
        // 设置值或者获取值
        this.propsWindow = new Proxy(fakeWindow, {
            set: (target, prop, value, receiver) => {
                const originValue = window[prop]
                if (!window.hasOwnProperty(prop)) {
                    this.addedPropsMapInSandbox.set(prop, value)
                } else if(!this.modifiedPropsOriginalValueMapInSandbox.has(prop)){
                    this.modifiedPropsOriginalValueMapInSandbox.set(prop, originValue)
                }
                this.currentUpdatePropsValueMap.set(prop,value)
                window[prop]= value
            },
            get: (target, prop, receiver) => {
                return window[prop]
            }
        })
​
    }
    setWindowProp(prop, value, isToDelete) {
        if (value === undefined && isToDelete) {
            delete window[prop]
        } else {
            window[prop] = value
        }
​
    }
​
    active() {
        // 恢复上一次该微应用处于运行状态时,对window 上做的所有应用的修改
        this.currentUpdatePropsValueMap.forEach((value, prop) => {
            this.setWindowProp(prop, value)
        })
    }
    // 失活 
    inactive() {
        // 还原window上的属性
        this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop) => {
            this.setWindowProp(prop, value)
        })
        // 删除在微应用运行期间 window 新增的属性
        this.addedPropsMapInSandbox.forEach((_, prop) => {
            this.setWindowProp(prop, undefined, true)
        })
    }
}
window.city="beijing"
let LegacySandbox01  = new LegacySandbox()
console.log('11111',window.city)
LegacySandbox01.active()
LegacySandbox01.propsWindow.city ="shanghai"
console.log('2222',window.city)
LegacySandbox01.inactive()
console.log('3333',window.city)

proxySandbox(多例沙箱)

多例沙箱原理

激活沙箱后,每次对window 取值的时候,先从自己沙箱环境的fakeWindow 里面找,如果不存在,就从rawWindow(外部的window)里去找;当对沙箱内部window 对象赋值的时候,会直接操作fakeWindow,而不会影响到rawWindow

class ProxySandbox {
    proxyWindow
    isRunning = false
    active() {
        this.isRunning = true
    }
    inactive() {
        this.isRunning = false
    }
    constructor() {
        const fakeWindow = Object.create(null)
        this.proxyWindow = new Proxy(fakeWindow, {
            set: (target, prop, value, receiver) => {
                if(this.isRunning){
                    target[prop] = value
                }
            },
            get: (target, prop, receiver) => {
                return prop in target ?target[prop]:window[prop];
​
            }
        })
    }
}
window.city="beijing"
let proxySandbox01 = new ProxySandbox()
let proxySandbox02 = new ProxySandbox()
proxySandbox01.active()
proxySandbox02.active()
proxySandbox01.proxyWindow.city = 'shanghai'
proxySandbox02.proxyWindow.city = 'chengdu'
console.log("111111",window.city)
console.log("222222",proxySandbox01.proxyWindow.city)
console.log("333333",proxySandbox01.proxyWindow.city)
proxySandbox01.inactive()
proxySandbox02.inactive()
​
console.log("111111",window.city)
console.log("222222",proxySandbox01.proxyWindow.city)
console.log("333333",proxySandbox01.proxyWindow.city)
​