深入理解CommonJS规范

node采用的是CommonJS规范。每个文件就是一个单独的模块,拥有属于自身的独立做用域,变量以及方法等。这些对其余模块都是不可见的。CommonJS规范规定,每一个模块内部,module表明当前模块。module是一个对象,它有一个exports属性,也就是module.exports。该属性是对外的接口,把须要导出的内容放到该属性上。外部能够经过require进行导入。require导入的就是exports中的内容。node

该篇文章就手动实现如下require方法,经过手写的require方法拿到另外一个文件中的exports中的内容。json

首先,咱们先看一下node环境中标准的require方法是如何引用模块的。
新建文件夹,在文件夹中新建b.js。经过module.exports将内容导出。
b.js:浏览器

let str = 'b.js导出的内容';
module.exports = str;

而后新建另外一个文件,my-require.js。在my-require.js中引入b.js中的str。
my-require.js:安全

let str = require('./b.js');

console.log(str);

运行代码,能够看到。打印出了b的内容:b.js导出的内容。
以上是标准CommonJS中require的引用,接下来手动实现它:
首先梳理如下逻辑,require函数中传递的参数是一个路径,有路径再加上node的fs模块,咱们就能够读取到该文件。那有了该文件的内容,从该文件中获取exports就不是什么难事了。
上代码:app

let path = require('path');
let fs = require('fs');
let vm = require('vm');

/** 定义本身的require方法 myrequire() */
function myrequire(modulePath){
    let absPath = path.resolve(__dirname,modulePath);
    function find(absPath){
        try{
            fs.accessSync(absPath);
            return absPath;
        }catch(e){
            console.log(e);
        }
    }
    absPath = find(absPath);
    let module = new Module(absPath);
    loadModule(module);
    return module.exports;
}

function Module(id){
    this.id = id;
    this.exports = {}
}

function loadModule(module){
    let extension = path.extname(module.id);
    Module._extensions[extension](module);
}

Module._extensions = {
    '.js'(module){
        let content = fs.readFileSync(module.id, 'utf8');
        let fnStr = Module.wrapper[0]+content+Module.wrapper[1];
        let fn = vm.runInThisContext(fnStr);
        fn.call(module.exports,module.exports,module,myrequire);
    }
}

Module.wrapper = [
    '(function(exports,module,require,__dirname,__dirname){',
    '})'
];


let str = myrequire('./b.js');

console.log(str);

阅读顺序从上至下。首先 引入了path fs和vm模块。path和fs都不用说了,都懂。vm模块是node的核心模块。核心功能官方解释的是:ide

  • The vm module provides APIs for compiling and running code within V8 Virtual Machine contexts. The vm module is not a security mechanism. Do not use it to run untrusted code. The term "sandbox" is used throughout these docs simply to refer to a separate context, and does not confer any security guarantees.

意思大体是:vm能够使用v8的Virtual Machine contexts动态地编译和执行代码,而代码的执行上下文是与当前进程隔离的,可是这里的隔离并非绝对的安全,不彻底等同浏览器的沙箱环境。
其实vm模块在该本文中的做用就是执行字符串代码,这样理解就好。函数

首先,定义了一个myrequire的方法。该方法传入一个相对路径。在myrequire方法中第一步将相对路径转换为绝对路径。而后又经过一个find方法来校验该路径是否存在。接下来经过构造函数Module传入绝对路径,new出了实例module。
该构造函数Module传入了路径id,内部定义了属性exports={}。该属性就是文件导出的属性。ui

紧接着,经过loadModule方法传入了实例module,来加载该文件。在loadModule方法中,首先获取了文件名后缀.js。 把文件名后缀.js传给Module._extensions。在Module._extensions对象中,经过文件后缀名.js找到该文件类型的解析方法。并把实例module传递进去。
在该方法中,经过module.id路径和fs模块经过获取到该文件内容content。注意下一步。在该文件内容content的外面用(function(exports,modules,require,__dirname,__filename){})函数包裹了一层。这样作的目的是待会要执行该函数而且拿到其中的module.exports中导出的内容。可是咱们刚才经过fs读取到的文件内容仅仅是字符串,又包裹了一层空函数,仍是字符串。
接下来就要用到vm模块。该模块能够执行字符串代码。经过vm.runInthisContext()方法,将刚才获得的字符串传递进去。此时就获得了能够执行的方法fn。
那接下来就是执行该方法fn了。执行fn,把刚才的参数传递进去。注意当前this执行为module.exports。这样才能拿到module.exports中的内容。
最后在myrequire中末尾,返回了该exports内容。return module.exports。
好,接下来就是验证效果了。右键code run,或者浏览器中打开。能够看到:this

b.js导出的内容

拿到了文件b.js中的内容,而且打印了出来。
好,如今以及实现了最简单了require。但是,咱们并不知足于此。由于该require方法还有一些问题。好比说,还不能引用json文件,并且也没有考虑若是文件没有后缀的状况。接下来继续完善myrequire方法:code

let path = require('path');
let fs = require('fs');
let vm = require('vm');

/** 定义本身的require方法 myrequire() */
function myrequire(modulePath){
    let absPath = path.resolve(__dirname,modulePath);
    let ext_name = Object.keys(Module._extensions);
    let index = 0;
    let old_absPath = absPath;
    function find(absPath){
        try{
            fs.accessSync(absPath);
            return absPath;
        }catch(e){
            let ext = ext_name[index++];
            let newPath = old_absPath+ext;
            return find(newPath);
        }
    }
    absPath = find(absPath);
    let module = new Module(absPath);
    loadModule(module);
    return module.exports;
}

function Module(id){
    this.id = id;
    this.exports = {}
}

function loadModule(module){
    let extension = path.extname(module.id);
    Module._extensions[extension](module);
}

Module._extensions = {
    '.js'(module){
        let content = fs.readFileSync(module.id, 'utf8');
        let fnStr = Module.wrapper[0]+content+Module.wrapper[1];
        let fn = vm.runInThisContext(fnStr);
        fn.call(module.exports,module.exports,module,myrequire);
    },
    '.json'(module){
        let content = fs.readFileSync(module.id, 'utf8');
        module.exports = content;
    }
}

Module.wrapper = [
    '(function(exports,module,require,__dirname,__dirname){',
    '})'
];


let str = myrequire('./b');

console.log(str);
console.log(myrequire('./a'));

在myrequire方法的第二行,先获取到Module._extensions中的全部后缀(目前有.js和.json),又声明了一个下标index,最后有保存了该路径old_absPath。 在find方法中,若是用户没有写文件后缀,就会自动拼接后缀。循环去查找,直到找到或者到最后也没找到。
在Module._extensions中新增了一个对象.json的方法。该方法较为简单。经过fs读取到文件并把文件内容放到module.exports中。ok,看下效果吧:

b.js导出的内容
{
    "name":"要引入的内容"
}

能够看到。正常拿到了b.js中的内容并且也读取到了a.json中的内容。至此,咱们就实现了CommonJS中的require方法。写文章不易,喜欢就点个👍吧 thx~

相关文章
相关标签/搜索