模块化-实现一个简单的CommonJS

139 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

一、需求:首先CommonJS中我们主要实现的内容是:

1.模块加载器: 解析文件地址,通过node,这次我们直接给一段代码传入,先不写解析部分

2.模块解析器: 执行文件内容

二、代码

1.创建一个class,定义传参和变量等

class Module {
  // 传入模块名称和文件内容(要执行的代码块)
  constructor(moduleName, source) {
    this.export = {}; // 定义模块要返回的export
    this.moduleName = moduleName;
    this.source = source;
  }
}

2.Require

》创建模块
》执行文件内容
》返回export
 require = (moduleName,source) => {
    // 创建一个模块
    const module = new Module(moduleName, source);
    
    // 执行文件得到结果
    const exports = compile(module, source);

    // 通过require返回结果
    return exports;
  }
》缓存
class Module {
  // 传入模块名称和文件内容(要执行的代码块)
  constructor(moduleName, source) {
    this.export = {}; // 定义模块要返回的export
    this.moduleName = moduleName;
    this.source = source;
    this.$cacheModule = new Map(); // 定义一个缓存变量
  }
  require = (moduleName,source) => {
    // 缓存中有这个模块的数据的话就返回缓存的exports
    if($cacheModule.has(moduleName)){
      return $cacheModule.get(moduleName).exports;
    }
    // 创建一个模块
    const module = new Module(moduleName, source);

    // 执行文件得到结果
    const exports = compile(module, source);

    // 把module存入缓存
    this.$cacheModule.set(moduleName, module);

    // 通过require返回结果
    return exports;
  }
}
》IIFE

声明完之后便直接执行的函数,这类函数通常是一次性使用的,因此没必要给这类函数命名,直接让它执行就好了 IIFE的作用就是防止变量全局污染,以及保证内部变量的安全 他是通过模仿一个私有作用域,用匿名函数作为一个“容器”,“容器”内部可以访问外部的变量,而外部环境不能访问“容器”内部的变量,所以 ( function(){…} )() 内部定义的变量不会和外部的变量发生冲突,俗称“匿名包裹器”或“命名空间”

通过IIFE让内部代码不会污染全局,但在IIFE中能访问到全局变量。

(function(arg1,arg2){...})(a,b)
 $wrap = (code) => {
    // 这里传入的参数就是想以后抛出到文件模块中用的接口
    return `function(module, exports, require){${code}}`
  }

》然后现在就是这个code到底怎么执行了

这里我们要创建一个沙箱环境执行code, 因为我们要保证code执行时:1.不能访问闭包的变量 ,2.不能访问全局的变量 ,3.只能访问我们传入的变量 这里我们通过new Function() 不能访问闭包,

const func1 = () => {
    const a = 123;
    return function() {
        const b = 456;
        console.log(a,b,window); // 123,456,window对象
        const func = new Function('obj',
			'console.log(obj.a + obj.b, window);'+  // 579, window对象
			'console.log(a,b)' // a,b都不能访问(即不能访问)
		); 
        return func({a:123,b:456});
    }
}
func1()()

通过with()包裹的对象,会被放到原型链的顶部,而且底层是通过 in 操作符判断的.,所有变量都会通过in从传入的对象中去取,但也能取到全局变量。

const obj = {a: 123, b: 456}
with(obj){
    console.log(obj)  // {a: 123, b: 456}
    console.log(a+b)  // 579
}

通过proxy去拦截with的in操作符,让with在取变量的过程中(底层),所有除了白名单以外的变量都通过in操作,如果没有就返回underfined

 // 代理
      const proxiedObject = new Proxy(sandbox, {
        // 专门处理 in 操作符的
        has(target, key) {
          if (!whiteList.includes(key)) {
            return true;
          }
        },
        get(target, key, receiver) {
          if (key === Symbol.unscopables) {
            return void 0;
          }
          return Reflect.get(target, key, receiver);
        }
      });

所以一个简单的沙箱如下

$runInThisContext = (code,whiteList=['console']) => {

  const func = new Function('sandbox', `with(sandbox){${code}}`);

  return function(sandbox){
    if(!sandbox || typeof sandbox !== 'object') {
      throw error("sandbox parameter must be a object")
    }
    const proxiedObject = new Proxy(sandbox, {
      has(target,key) {
        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(proxiedObject)
  }
}

》最后在沙箱环境下传入code,执行代码

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

3.验证

const m = new Module();

// a.js
const aSourceCode = `
  const b = require('b.js', 'const a = require("a.js"); console.log("module a:", a); exports.action = function() {console.log("excute action from B successfully!")}');
  b.action();
  exports.action = function() {
    console.log("excute action from A successfully!")
  }
`
m.require('a.js',aSourceCode)
// module a: {}
// excute action from B successfully!

4.完整代码

class Module {
  constructor(moduleName, source) {
    this.exports = {};
    this.moduleName = moduleName;
    this.$cacheModule = new Map();
    this.$source = source;
  }

  /**
   * @description: require方法
   * @param {string} moduleName 其实就是路径信息
   * @param {string} source 文件的源代码,因为省略了加载器解析路径得到源代码的过程
   * @return {object} export出去的对象
   */
  require = (moduleName, source) => {
    if (this.$cacheModule.has(moduleName)) {
      return this.$cacheModule.get(moduleName).exports;
    }

    // 创建模块
    const module = new Module(moduleName, source);

    // 执行文件
    const exports = this.compile(module, source);

    // 缓存
    this.$cacheModule.set(moduleName, module);

    return exports;
  };
/**
 * @description:  IIFE 拼一个闭包
 * @param {string} code 代码字符串
 * @return {*}
 */
$wrap = (code) => {
  const wrapper = [
    'return (function(module,exports,require) {',
    '\n})'
  ]
  return wrapper[0] + code + wrapper[1]
}

/**
 * @description: 简单实现一个能在浏览器跑的解释器 vm.runInThisContext
 * @param {string} code
 * @return {*}
 */
$runInThisContext = (code,whiteList=['console']) => {

  const func = new Function('sandbox', `with(sandbox){${code}}`);

  return function(sandbox){
    if(!sandbox || typeof sandbox !== 'object') {
      throw error("sandbox parameter must be a object")
    }
    const proxiedObject = new Proxy(sandbox, {
      has(target,key) {
        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(proxiedObject)
  }
}




  /**
   * @description: 执行文件
   * @param {*}
   * @return {*}
   */
  compile = (module, source) => {
    const compiler = this.$runInThisContext(this.$wrap(source))({});
    compiler.call(module, module, module.exports, this.require)
    return module.exports;
  };
}


const m = new Module();

// a.js
const aSourceCode = `
  const b = require('b.js', 'const a = require("a.js"); console.log("module a:", a); exports.action = function() {console.log("excute action from B successfully!")}');
  b.action();
  exports.action = function() {
    console.log("excute action from A successfully!")
  }
`
m.require('a.js',code)