Qiankun奇思妙想的沙盒知识点

764 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情

js沙箱

首先先大致描述下js沙盒的几种实现思路

  1. 快照沙盒就是在激活时把全局上的属性都备份一遍,然后在取消激活时把备份的赋值回去,但会把当前的激活的内容也备份一份,待激活后赋值回去
  2. 单例代理沙盒就是代理全局变量window,然后在修改时赋值到存储对象上,把原值也保存一份,对比快照沙盒在性能上更优,且占用的内存也小

以上两种都是对全局变量window进行操作,会window污染,并且无法多个沙盒同时存在

  1. 多例沙盒就是基于Proxy+with的方式实现,不会污染全局window,支持多个沙盒同时使用,拥有单例沙盒的优点。

以上这些沙盒在网上已经有很多不错的资料了,如:

微前端JS沙箱实现的几种方式

但在多例沙盒的实现上基本上都是比较粗略的方式,没有具体怎么防止全局变量的隔离,如

const proxy = {};
window.a = '222';
(function(window) {
    window.a = '111';
    console.log(window.a, a); // 111 222
})(proxy);
console.log(window.a); // 222

使用闭包的方式隔离变量,但无法隔离全局变量

而需要彻底隔离this、全局变量就要使用Proxy+with+bind

with

JavaScript 查找某个未使用命名空间的变量时,会通过作用域链来查找,作用域链是跟执行代码的 context 或者包含这个变量的函数有关。'with'语句将某个对象添加到作用域链的顶部,如果在代码块中有某个未使用命名空间的变量,跟作用域链中的某个属性同名,则这个变量将指向这个属性值。如果沒有同名的属性,则将拋出ReferenceError异常。

const proxy = {
    a: '333'
};
window.a = '222';
with(proxy) {
    a = 'zhan';
    console.log(a); // zhan
}
console.log(a); // 222

但with替换的对象没有当前要取的值会向上查找,导致值修改的是上层的,而不是给替换的对象添加一个新的属性值

const proxy = {};
window.a = '222';
with(proxy) {
    a = 'zhan';
    console.log(a); // zhan
}
console.log(a); // zhan

但这种情况可以proxy的has能解决

Proxy

Proxy可以理解为对目标对象的访问和操作之前进行一次拦截。提供了这种机制,所以对目标对象进行修改和过滤的操作。

const obj = {
    a: 1
};
const proxy = new Proxy(obj, {
    get(target, key) {
        console.log(`你访问的属性是${key}`);
        return target[key];
    },
    set: (target, key, value) => {
        console.log(`你设置${key}属性是${value}`);
        target[key] = value;
        return true;
    },
})
console.log(proxy.a);
proxy.a = 2;

Proxy实际是对对象属性上的值用自己的定义覆盖了语言的原始定义。

同一个Proxy可以拦截多个操作,只需要在第二个参数配置添加

Proxy 支持的拦截操作

  • get(target, propKey, receiver) 拦截对象属性读取
  • set(target, propKey, value, receiver) 拦截对象的属性设置
  • has(target, propKey) 拦截propkey in proxy
  • deleteProperty(target, propKey) 拦截delete proxy[propKey]
  • ownKeys(target)
  • getOwnPropertyDescriptor(target, propKey) 返回对象属性的描述对象拦截
  • defineProperty(target, propKey, propDesc)
  • proventExtensions(target)
  • getPrototypeOf(target)
  • isExtensible(target)
  • setPrototypeOf(target, proto)
  • apply(target, object, args)
  • construct(target, args) 拦截 proxy 实例作为构造函数调用的操作

以上不一一描述,主要描述下has,因为这关系到with访问或者修改不存在的变量时获取或者修改的with外的值

has() 方法是针对 in 操作符的代理方法。

这个钩子可以拦截下面这些操作:

  • 属性查询:foo in proxy
  • 继承属性查询:foo in Object.create(proxy)
  • with 检查: with(proxy) { (foo); }
  • Reflect.has()

即proxy+with可以拦截with内添加的新值

const obj = {};
const proxy = new Proxy(obj, {
    has(target, key) {
        console.log(`拦截${key}`);
        if (key === 'console') {
            return false;
        }
        return true;
    },
    get(target, key) {
        return target[key];
    },
    set: (target, key, value) => {
        target[key] = value;
        return true;
    },
})
window.a = '222';
with(proxy) {
    a = 'zhan';
    console.log(`with环境a=>${a}`); // zhan
}
console.log(`外部环境a=>${a}`); // 222

从以上的with例子和现在加了proxy的例子,可以看出新增值不在影响外部环境

当然要实现一个真正的隔离的沙盒,还是需要更加精细的逻辑在拦截属性里实现

多例沙盒demo

class ProxySandbox {
    active() {
        this.sandboxRunning = true;
    }
    inactive() {
        this.sandboxRunning = false;
    }
    constructor() {
        const rawWindow = window;
        const fakeWindow = {};
        const proxy = new Proxy(fakeWindow, {
            has(target, key) {
                if (key === "consloe") {
                    return false;
                }
                return true;
            },
            set: (target, prop, value) => {
                if (this.sandboxRunning) {
                    target[prop] = value;
                    return true;
                }
            },
            get: (target, prop) => {
                // 加固,防止逃逸
                if (prop === Symbol.unscopables) {
                    return undefined;
                }
                // 如果fakeWindow里面有,就从fakeWindow里面取,否则,就从外部的window里面取
                let value = prop in target ? target[prop] : rawWindow[prop];
                return value;
            },
        });
        this.proxy = proxy;
    }
}


window.abc = "男";

let proxy1 = new ProxySandbox();
proxy1.active();

(function(window) {
    with(window) {
        abc = "00001";
        console.log(abc); // 00001
    }
}.bind(proxy1.proxy)(proxy1.proxy));

console.log("外部window.abc=>1", window.abc); // 男

let proxy2 = new ProxySandbox();
proxy2.active();

(function(window) {
    with(window) {
        abc = "111";
        console.log(abc); // 111
    }
}.bind(proxy2.proxy)(proxy2.proxy));

console.log("外部window.abc=>2", window.abc); // 男

demo

css隔离

shadow DOM

可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。

const elem = document.querySelector('#host'); 

const shadowRoot = elem.attachShadow({mode: 'open'}); 

const p = document.createElement('p'); 

shadowRoot.appendChild(p); 

p.textContent = 'Hello!';

scoped css

遍历所有的style标签,分别获取每个标签内所有样式的选择器和css文本并对其进行处理,根选择器替换为前缀,普通的选择器前面直接加上前缀。

但这个方案目前在qiankun还是实验方案,而且需要遍历,而在样式,大多数项目都是有很多冗余无效的样式代码,这也给需要遍历的内容更加需要消耗的时间,对于一个应用的加载需要耗时也增长,在性能上肯定比较差,不过有预加载的模式,在体验上理论上也不会差

资料

github.com/umijs/qiank…

github.com/kuitos/impo…

learnku.com/articles/59…