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 的。
posted @ 2023-05-28 15:55  kongshu  阅读(302)  评论(0编辑  收藏  举报