前端沙箱环境 与 前端模块化 Common Js 的简单实现

223 阅读2分钟

如何创建一个沙箱环境

new Function

new Function 构建的函数内部不可以访问闭包,不能访问局部变量,只可以访问全局变量

   // 闭包
    function aa (){
    let name = 'jack'
    return function(){
      let str = "console.log('Hello' + name +'!') ; console.log(typeof name)";
      let func = new Function(str);
      func()
    }
  }
  let bb = aa()
  bb()
  // 结果
  // Hello!
  //string
  
 // 局部变量
 function aa (){
    let name = 'jack'
      let str = "console.log('Hello' + name +'!') ; console.log(typeof name)";
      let func = new Function(str);
      func()
  }
 aa()
 // 结果
 // Hello! 
 //string
 
 // 全局变量
  let name = 'Bob'
  function aa (){
      let str = "console.log('Hello' + name +'!') ; console.log(typeof name)";
      let func = new Function(str);
      func()
  }
  aa()
  // 结果
 // HelloBob! 
 //string
 
 let name = 'Bob'
  function aa (){
    return function(){
      let str = "console.log('Hello' + name +'!') ; console.log(typeof name)";
      let func = new Function(str);
      func()
    }
  }
  let bb = aa()
  bb()
  // 结果
 // HelloBob! 
 //string

with

with传进的对象,会被放到包裹代码的原型链顶部, in操作符

function aa(a,b){
    with(b){
      console.log(a)
    }
  }
  aa(1,{a: 2})
  // 2

proxy

借助proxy,可以对in操作进行劫持 用例搬自MDN


const handler1 = {
  has(target, key) { // 对in操作符的一个钩子
    if (key[0] === '_') {
      return false;
    }
    return key in target;
  }
};

const monster1 = {
  _secret: 'easily scared',
  eyeCount: 4
};

const proxy1 = new Proxy(monster1, handler1);
console.log('eyeCount' in proxy1);
// expected output: true

console.log('_secret' in proxy1);
// expected output: false

console.log('_secret' in monster1);
// expected output: true

new Function + with + Proxy

new Function + with + Proxy 实现一个不能访问全局,局部,闭包,只能访问传入变量的沙箱

function sandbox(){
    let obj = { require: 'this is require', export: 'this is export' }
    const proxyObj = new Proxy(obj, {
      has(target, key){
        return true
      },
      get(target, key){
        return Reflect.get(target, key)
      }
    })
    let str = `with(obj){ return obj }`
    
    // 这里面不能用console.log,因为我们传入的对象中并没有console对象
    // 此时的with里面的代码只能拿到我们传入的proxyObj中拥有的属性的值
    
    const func = new Function('objs', `with(objs){ return require }`)
    return func(proxyObj)
  }
  let res = sandbox()
  console.log(res)
  // expexted output: this is require

Common Js 的简单实现

Common Js的特点

  1. commonJs的引用是对值的引用,是执行后的文件
  2. commonJs重复引用文件不会重复执行文件,说明被引用的文件结果会被存储起来,再次引用时直接拿值
  3. exports导出的对象是一个对象
  4. module.exports导出的是定义的本身

Common Js的实现

  • 实现一个new Function + with + Proxy 配合的沙箱
  • 定义一个有exports 和 require 属性的对象, 把这个对象作为参数传进沙箱
  • 拿到目标文件的代码,放进with中执行
  • 返回执行后的结果
class Module {
    constructor(moduleName, source){
      this.exports = {}
      this.moduleName = moduleName
      this.$cacheModule = new Map()
      this.$source = source
    }

    /**
     * @param {string} module 模块的名称: 路径
     * @param {string} source 文件源代码
    */

    require = (moduleName, source) => {
      if(this.$cacheModule.has(moduleName)){
        return this.$cacheModule.get(moduleName).exports
      }
      const module = new Module(moduleName, source)
      this.$cacheModule.set(moduleName, module)
      const exports = this.compile(module, source)
      return exports
    }

    compile = (module, source) => {
      const compiler = this.$runInThisContext(this.$wrap(source))({})
      compiler.call(module, module, module.exports, this.require)
      return module.exports
    }

    $wrap = (code) => {
      const wrapper = [
        'return (function (module, exports, require){',
        '\n})'
      ]
      return wrapper[0] + code + wrapper[1]
    }

    $runInThisContext = (code, whiteList=['console']) => {
      const func = new Function('sandbox', `with(sandbox){ ${code} }`)
      return function(sandbox){
        if(!sandbox || typeof sandbox !== 'object'){
          throw new Error('sandbox paramter must be an object')
        }
        const proxyObject = new Proxy(sandbox, {
          has(target, key){
          // 将consosle对象放进白名单,这样就会从全局对象中找console
            if(!whiteList.includes(key)){
              return true
            }
          },
          get(target, key, receiver){
            if(key === Symbol.unscopables){
              return void 0
            }
            return Reflect.get(target, key, receiver)
          }
        })
        return func(proxyObject)
      }
    }
  } 

  const m =  new Module()
const sourceCodeFromAModule = `
  const b = require('b.js', 'exports.action = function(){ console.log("execute action from B module successfully")}')
  b.action()
`
m.require('a.js', sourceCodeFromAModule)


// expexted output: execute action from B module successfully