js 沙盒的实现原理
js 沙盒的实现原理
最近研究微前端,qiankun 框架的源码。看到了沙盒的概念,于是研究了一下沙盒的原理及实现方案。记录一下。
沙盒的原理
- 为什么我们需要沙盒
在浏览器中,全局的this 实际指向的是window。如果我们运行js,我们有可能会往window 这个对象中写入一些数据。沙盒就是为了隔离window 而存在的。举个例子,我们引用了两个包,这两个包中都使用了同一个变量,那么这样是会带来问题.如下面的例子所示,可能moduleA/B 实际都没有初始化。
// moduleA
if(initialized){
console.log('moduleA ok');
}
// moduleB
if(initialized){
console.log('module B ok');
}
// moduleC
initialized=true;
// 这会意外的改变moduleA/B 的状态。
- js 沙盒的目的就是为一段js 代码的执行创建一个独立的上下文,例如window.
如何手撸一个简单的沙盒呢?
- 借助js 中的
with(context){}
,介入js 代码的变量引用解析的过程。with介入后,代码块中的变量解析过程会将context 做为第一优先级去查找。 - 借助js 中的Proxy,通过将上下文进行拦截,实现上层上下文的穿透。
- 借助iframe, iframe是浏览器提供的最完美的js 沙盒。
- 实现代码如下
- 首先我们需要创建一个沙盒的代理
function createSandbox(sharedState){
// 创建iframe 元素,获取contentWindos,
const iframe= document.createElement('iframe',{url:'about:blank'});
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;
const sandbox = new Proxy(sandboxGlobal,{
// 由于代理对象只能代理创建的时刻的属性,所以我们通过has,的方法,支持创建新的属性。
has(target,key){
if([...sharedState].includes(key)){
return false;
}
return true;
},
get(target,prop,reciver){
// 这个条件非常重要,首先this === window, this.window===window
// 如果我们通过代理对象去访问window,js 会报illegal Invocation 的错误
// 所以我们这边将这三个参数进行拦截,返回沙盒本身
if(['window','self','globalThis'].includes(prop)){
return sandbox;
}
return target[prop];
},
});
const destory=()=>{
document.body.removeChild(iframe);
}
return {sandbox,destory};
}
- 其次我们需要将被执行的代码插入作用连解析的代码
function withYourCode(code){
// 如下的两种方式都是可以的
const withWrap=`(function(window){with(window){${code}}})`;
return (0,eval)(withWrap);
// 第二种方式
// const withWrap = `with(window){${code}}`;
// return new Function('window',withWrap);
}
- 最后我将上面的两种代码封装起来
function runCode(code,sharedState=[]){
const {sandbox,destory} = createSandbox(sharedState);
withYourCode(code).call(sandbox,sandbox);
destory();
}
这样我们一个简单能用的沙盒就成了。
遇到的坑。
- 创建代理的时候,没有加get 的那段拦截,结果,代码种访问window的时候报错了。
runCode('console.log(window)')
。原因是window 这个对象有点特殊,window.window===window
.创建window对象的代理时有问题。后来我想要不在has 方法里面把window属性返回*alse,在 withYourCode方法中传递window。 代码示例如下:
// 有问题的代码
const sandbox = new Proxy(sandboxGlobal,{
has(target,key){
if([...sharedState,'window','self','globalThis'].includes(key)){
//这样也是不行的因为window 的descriptor configurable 是false,不能这么做。如果不是因为这个原因,这个方案是可行的。因为被执行的代码可以从我们顶层传递的window 拿到这个变量。
return false;
}
return true;
},
});
function withYourCode(code){
// 如下的两种方式都是可以的
const withWrap=`(function(window){with(window){${code}}})`;
return (0,eval)(withWrap);
}
沙盒的局限性
- 首先沙盒依赖with语法,这个会有一定的性能损耗,没有经过实践
- 沙盒也是可以穿透的,因为我们代理的是window 这个对象的直接属性。如果我们通过window.parent,其实也是可以拿到父级的window,那还是有可能污染全局的window 的。