携手创作,共同成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
js沙箱
首先先大致描述下js沙盒的几种实现思路
- 快照沙盒就是在激活时把全局上的属性都备份一遍,然后在取消激活时把备份的赋值回去,但会把当前的激活的内容也备份一份,待激活后赋值回去
- 单例代理沙盒就是代理全局变量window,然后在修改时赋值到存储对象上,把原值也保存一份,对比快照沙盒在性能上更优,且占用的内存也小
以上两种都是对全局变量window进行操作,会window污染,并且无法多个沙盒同时存在
- 多例沙盒就是基于Proxy+with的方式实现,不会污染全局window,支持多个沙盒同时使用,拥有单例沙盒的优点。
以上这些沙盒在网上已经有很多不错的资料了,如:
但在多例沙盒的实现上基本上都是比较粗略的方式,没有具体怎么防止全局变量的隔离,如
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); // 男
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还是实验方案,而且需要遍历,而在样式,大多数项目都是有很多冗余无效的样式代码,这也给需要遍历的内容更加需要消耗的时间,对于一个应用的加载需要耗时也增长,在性能上肯定比较差,不过有预加载的模式,在体验上理论上也不会差