JS上下文隔离原理和实现方案

168 阅读2分钟

定位于微前端方向,我们知道微前端里面有个很重要的一环就是如何处理各个子应用的上下文环境相互独立,在独立的同时还要维持各个子应用的上下文环境状态。这里面涉及到一个技术点就是js上下文环境的相互隔离。制造js的沙箱环境,需要先知道这几个知识点,with关键字,new Function,proxy,iframe,reflect。

  • with,用于改变作用域链,如下代码所示,with扩展了作用域链,name变量优先从withObj中查找,我们再看下age,由于age在withObj中不存在,所以会沿着作用域链查找到全局作用域上,那我们在全局作用域上定义了一个age变量,就找到这个变量了,如果全局作用域找不到的话,就会发生报错了。
const age = 12;const name = 'linjian';
const withObj = {  name: 'maclerylin',};
with (withObj) {  console.log(name);  
console.log(age);  
console.log(gender);}
// 输出结果
MacBook-Pro:utils linjian$ node test.js 
maclerylin
12
/Users/linjian/documentmaclery/project/basic-prj/micro-front-prj/hand-write/src/utils/test.js:15
  console.log(gender);
              ^

ReferenceError: gender is not defined
  • new Function,执行代码功能类似于eval,但是eval是可以访问到局部变量,而new Function是不可以访问到局部变量的。我们看看如下代码:
const age = 12;
const name = 'linjian';
eval(`  console.log(name);`)
new Function(`  console.log(name);`)();
// 输出结果
MacBook-Pro:utils linjian$ node test.js 
linjian
undefined:4
  console.log(name);
              ^

ReferenceError: name is not defined
  • proxy,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。看下如下代码:
const withObj = {  name: 'maclerylin',};
const proxy = new Proxy(withObj, {  
    set(obj, prop, value) {    
        console.log('set 劫持');  
    },  
    get(obj, prop) {    
        console.log('get 劫持');    
        return obj[prop];  
    }
})
proxy.name = 'hanmeimei';
console.log(proxy.name);
// 输出结果
MacBook-Pro:utils linjian$ node test.js 
set 劫持
get 劫持
maclerylin

对于属性的set、get操作可以很精确的捕捉到。

  • reflect,用于拦截一个对象的原始操作,看下如下例子,reflect代理对象的set操作,用法非常简单,但是似乎和 withObj.name = 'hanmemei';的操作结果是一样的。是否存在差别?
const withObj = {  name: 'maclerylin',};
Reflect.set(withObj, 'name', 'hanmeimei');
console.log(withObj.name)
// 输出结果 
MacBook-Pro:utils linjian$ node test.js 
hanmeimei

接下来,我们看下差别。如下代码中,我们看到我们定义了parent和child两个对象,每个对象中都有_name属性,child继承parent对象的代理,此时访问name属性,target指向proxy。

const parent = {  _name: 'maclerylin',  get name() {    
    return this._name;  
}};
const proxy = new Proxy(parent, {  
    set(target, prop, value) {    
        target[prop] = value  
    },  
    get(target, prop, receiver) {    
        return target[prop];  
    }
});
const child = {  _name: 'hanmemei',};
Object.setPrototypeOf(child, proxy);
console.log(child.name);
// 输出
MacBook-Pro:utils linjian$ node test.js 
maclerylin

那我们修改一下,如下代码我们通过在reflect中传入receiver,代码执行过程中可以正确的找到调用对象。

const parent = {  
    _name: 'maclerylin',  
    get name() {    
        return this._name;  
    }
};
const proxy = new Proxy(parent, {  
    set(target, prop, value) {    
        target[prop] = value  
    },  
    get(target, prop, receiver) {    
        - return target[prop];    
        + return Reflect.get(target, prop, receiver);  
    }
});
const child = {  _name: 'hanmemei',};
Object.setPrototypeOf(child, proxy);
console.log(child.name);
// 输出结果
MacBook-Pro:utils linjian$ node test.js 
hanmemei
  • iframe, 可嵌入窗口,每个iframe都拥有独立的上下文环境,包括dom文档、js、css都是跟容器相互隔离,所以通过iframe来隔离js上下文环境,往往能做到非常彻底的隔离。

我们在理解了以上几个知识点后,接下来,我们将介绍js隔离的几种方式。

  • new Function + Proxy + Reflect + 白名单
const windowProxy = new Proxy(window, {
  get(target, prop, revceiver) {
    if (prop === 'origin') {
      return 'origin'
    }
    return Reflect.get(target, prop, revceiver);
  }
});
const code = 'console.log(origin)';
const sandbox = function ({ windowProxy }, code) {
  return new Function(`
    with(windowProxy) {
      ${code};
    }
  `)()
}
sandbox({ windowProxy }, code);
// 输出结果
origin

这里我们通过在proxy中设置白名单方式,拦截对window对象的访问限制,达到一定程度上的隔离效果,但是这种方式很依赖于白名单的配置,框架方和使用者一直处于防守和攻击的状态,很难从根上解决隔离问题

  • new Function + Proxy + Reflect + iframewindow,这种方案通过iframe,强制隔离window,类似于将window对象深拷贝。
const iframe = document.createElement('iframe');
const windowIF = iframe.contentWindow;
const windowProxy = new Proxy(windowIF, {  
    get(target, prop, revceiver) {    
        if (prop === 'origin') {      
            return 'origin'    
        }    
        return Reflect.get(target, prop, revceiver);  
    }
});
const code = 'console.log(window.origin)';
const sandbox = function ({ windowProxy }, code) {  
    return new Function(`    
        with(windowProxy) {      
            ${code};    
        }  
    `)()
}
  • TC39提出了最新的草案,Realm API,提供沙箱功能(sandbox),允许隔离代码,防止那些被隔离的代码拿到全局对象

    const globalOne = window;
    const globalTwo = new Realm().global;
    globalOne.evaluate('3 + 2') // 5
    globalTwo.evaluate('3 + 2') // 5
    

总结一下,总共有三种方案可用于js上下文隔离。