2.2 Node模块的实现
Node并不是完全按照规范实现,而是进行了一定的取舍,并同时增加了少许自身需要的特性。
Node引入模块时,需要经历如下几个步骤:
1.路径分析。
2.文件定位。
3.编译执行。
Node中提供的模块分为两类,一类是node自身提供称为核心模块;另一类是用户自定义称为文件模块。
Node的核心模块,在编译源码时,就已经编译进二进制可执行文件。在Node启动时,核心模块会加载到内存,这部分模块引入时,文件定位和编译执行这两步会省略,并在路径分析中优先判断,所以它的加载速度很快。
文件模块则在运行时动态加载,需要完整的经历以上三个步骤,速度相对比核心模块慢。
2.2.1 优先从缓存加载
与前端浏览器的实现一样,对已经引入过的模块,Node会暂存在内存中,以加速第二次引入模块的速度。但不同的是,浏览器只缓存文件,而Node缓存的是编译和执行后的对象。
2.2.2 路径分析和文件定位
标识符有几种形式,针对不同的标识符,查找和定位有一定的差异。
1.模块标识符分析
require接收一个标识符当作参数,模块标识符在Node中主要分为以下几类:
*.核心模块,比如http, fs, path等。
*…或者…开始的相对路径文件模块。
*.以/开始的绝对路径文件模块。
*.非路径形式的文件模块。
核心模块的优先级仅次于缓存,它在Node编译源码时就已经编译为二进制代码。如果加载一个与核心模块相同的标识符会失败,必须选择一个不同的标识符或者换用路径的方式。
以.或…开始的标识符,都会被当作文件模块来处理。require会把路径转换为真实路径,并以路径为索引,将编译执行后的结果存放到缓存中,以加快二次引用,由于指明了具体的路径,查找需要花费一定的时间,速度较核心模块慢。
自定义模块是非核心模块,也不是路径形式的标识符,查找速度最慢。
2.模块路径的生成规则(module.paths)
*.当前文件目录下的node_module目录。
*.父目录下的node_module目录。
*.父目录的父目录的node_module目录。
*.沿路径向上逐级递归,直到根目录下的node_module目录。
比如
'/home/jackson/research/node_modules',
'/home/jackson/node_modules',
'/home/node_modules',
'/node_modules',
可以看出,当前文件路径越深,模块查找越耗时,这是自定义模块加载慢的根本原因。
3.文件定位
文件扩展名的分析
require在分析扩展名的时候,会出现标识符中不包含文件扩展名的情况。CommonJS规范也允许在标识符中不包含文件扩展名,在这种情况下,Node会按.js,.json,.node等次序补足扩展名。
目录分析和包
require在通过分析文件扩展名后,可能没有找到对应的文件,但却得到一个目录,此时Node会将目录当作一个包来处理,首先Node在当前目录下查找package.json,通过JSON.parse分析,从中取出main属性指定的文件名进行定位。
如果main指定的文件名错误,或者根本没有package.json,Node会将index当作默认文件名,然后依次查找index.js,index.json,index.node。
2.2.3 模块编译
在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解析返回的结果。
每一种扩展文件,都对应一个解析该扩展文件的方法,比如输出如下代码:
console.log(require.extensions);
得到如下结果:
{’.js’:[function],’.json’:[function],’.node’:[function]}
1.javascript模块的编译(.js)
CommonJS规范定义,每个模块文件中存在require,exports,module这三个变量,但是它们在模块中并没有定义。
甚至在Node的API文档中,每个模块中还有__filename__,__dirname这两个变量,它们在哪里定义呢?
事实上,在编译的过程中,Node对获取的Javascript文件内容进行了头尾包装,如下:
((function (exports,require,module,__filename__,__dirname){
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius;
}
})
如上我们看到,在Node编译整个js文件时,会把整个js文件进行包装,将如上几个参数当作方法参数传入,从而在整个js文件中使用时,能够被引用到。其中在exports中定义的属性和方法,都可以被外部调用到,但在模块中的其余变量或属性则不可直接被调用。
2.c/c++模块的编译(.node)
Node调用process.dlopen()方法进行加载和执行。dlopen在不同的平台实现不同,目前node是通过libuv兼容处理。
另外.node文件并不需要编译,它是在编写完c/c++模块之后编译生成。
c++模块给Node使用者带来的优势主要是执行效率方面,劣势则是c/c++模块的编写门槛比Javascript高。
3.json文件的编译(.json)
Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象。然后将它赋给模块对象的exports,以供外部调用。JSON文件主要用在对项目的配置上,直接调用require就可以直接引用。