深刻理解CommonJS规范

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

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

首先,我们先看一下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就不是什么难事了。
上代码:


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的核心模块。核心功能官方解释的是:

  • 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={}。该属性就是文件导出的属性。

紧接着,通过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,或者浏览器中打开。可以看到:

b.js导出的内容

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


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~

    原文作者:Funky_Tiger
    原文地址: https://segmentfault.com/a/1190000018464813
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞