深刻理解CommonJS规范

栏目: JavaScript · 发布时间: 6年前

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

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方法。写文章不易,喜欢就点个:+1:吧 thx~


以上所述就是小编给大家介绍的《深刻理解CommonJS规范》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Design for Hackers

Design for Hackers

David Kadavy / Wiley / 2011-10-18 / USD 39.99

Discover the techniques behind beautiful design?by deconstructing designs to understand them The term ?hacker? has been redefined to consist of anyone who has an insatiable curiosity as to how thin......一起来看看 《Design for Hackers》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

URL 编码/解码
URL 编码/解码

URL 编码/解码

SHA 加密
SHA 加密

SHA 加密工具