模块编译
在Node中,每个文件模块都是一个对象,它的定义如下:
function Module(id, parent){
this.id = id;
this.exports = {};
this.parent = parent;
if(parent && parent.children){
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}
编译和执行是引入文件模块的最后一个阶段。定位到具体文件后,Node会新建一个模块对象,然后通过路径载入并编译。对于不同的文件扩展名,其载入的方式也有所不同,具体如下。
.js文件。通过fs模块同步读取文件后编译执行。
.node文件。这是用C/C++编写的扩展文件,通过dlopen()方法加载最后的编译生成的文件。
.json文件。通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
其余扩展名文件。它们都被当作.js文件解析。(因为node只能解析js文件,其他文件最后都会被转化成js文件,故当其余扩展名文件出现时,node无法识别,故将其认为是默认扩展名进行解析,即.js)
每一个编译成功的模块都会将文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。
根据不同文件扩展名,node会调用不同的读取方式,如:.json文件调用如下:
Module._extensions['.json'] = function(module,filename){
var content = NativeModule.require('fs').readFileSync(filename,'utf8');
try{
module.exports = JSON.parse(strinpBOM(cntent));
}catch(err){
err.message = filename + ':' + err.message;
throw err;
}
}
其中,Module._extensions会被赋值给 require() 的extensions属性,所以通过在代码中访问require.extensions可知道系统中已有的加载方式。编写代码测试一下:
//新建index.js文件
console.log(require.extensions);
//运行node index.js,输出
//[Object: null prototype] { '.js': [Function], '.json': [Function], '.node': [Function] }
如果想对自定义的扩展名进行特殊的加载,可以通过类似require.extensions[’.ext’]的方式实现。早期的CoffeeScript文件就是通过添加require.extensions[’.coffee’]扩展方式来加载的。但是从v0.10.6版本开始,官方不鼓励通过这种方式进行自定义扩展名的加载,而是期望先将其它语言或文件先编译成js文件后再进行加载,这样做的好处是不将繁琐的编译加载等过程引入node的执行过程。
在确定文件的扩展名后,node将调用具体的编译方式来将文件执行后返回给调用者。
注:我们都知道CommonJS模块规范中,每个模块文件都存在require、exports、module、_filename、_dirname
这5个变量却不知其从何而来。若是把直接定义模块的过程放在浏览器端,势必会存在污染全局变量的情况,故其不可能。
事实上,在编译过程中,node会对获取的js文件内容进行包装,具体如下:
(function(require、exports、module、_filename、_dirname){
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius;
}
})
这样每个模块文件之间都进行了作用于隔离。包装之后的代码会通过vm原生模块的runInThisContext() 方法执行,类似于eval,只是具有明确上下文,不污染全局,返回一个体的function对象。最后,将当前模块对象的exports属性,require()方法,module(模块对象本身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。
这就是为什么这些变量并没有在每个模块声明却可以使用的原因。在执行后,模块的exports属性被返回给了调用方。exports属性上的任何方法都可以被外部调用到,但是模块中的其余变量和属性则不可直接被调用。
至此,require、exports、module的流程已经完整,这就是Node对CommonJS模块规范的实现。
JSON文件的编译
json文件的编译是三种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用。
JSON文件在用作项目的配置文件时比较有用。如果你定义了JSON文件作为配置,那就不用调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响。
这里我们提到的模块编译都是指文件编译,即用户自己编写的模块。