nodeJs学习笔记-Node的模块实现
CommonJS规范
CommonJS 对于模块的定义十分的简单,主要分为模块引用,模块定义和模块标识3分部分。
模块引用
var math = require('math');
模块定义
在模块中,上下文提供require()方法来引入外部模块,对应引入的功能,上下文提供了exports对象用于导出当前模块的方法和变量,并且它是唯一导出的出口。在模块中,还存在一个一个module对象,它代表模块本身,而expirts是module的属性,在Node中,一个文件就是一个模块,将方法挂载在exports对象上作为属性就可以定义导出的方式。
// 方式一
// hello.js
function Hello(){
var name;
this.setName = function(thyName){
name = thyName
}
this.sayHello = function(){
console.log('hello ' + name);
}
}
exports.Hello = Hello;
// main.js
var Hello = require('./hello.js').Hello;
// 方式二
// hello.js
function Hello(){
var name;
this.setName = function(thyName){
name = thyName
}
this.sayHello = function(){
console.log('hello ' + name);
}
}
module.exports = Hello;
// main.js
var Hello = require('./hello.js')
PS:不可以通过对exports直接赋值替代对module,exports赋值。exports实际上只是一个和module.exports指向同一个对象的变量,它本身会在模块执行结束后释放,但是module却不会,因此只能通过指定module.exports来访问该接口
模块标识
模块标识实际上就是传递给require()方法的参数,他必须是符合小驼峰命名的字符串,或者是以.,..开头的相对路径,或者绝对路径。它可以没有文件后缀js
Node的模块实现
Node在实现上面并没有完全按照规范实现,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。
在Node中引入模块,需要经历三个步骤。
路径分析
文件定位
编译执行
在Node中,模块是分为两类的:一类是Node提供的模块,叫核心模块,一类是用户自己编写的模块,叫文件模块。
对于核心模块:核心模块部分在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动的时候,部分核心模块就直接被加载到了内存中,所以这部分核心模块引入的时候,文件定位和编译执行这两个步骤可以省略掉了,并且在路径分析中优先判断,所以它的加载速度是最快的。
对于文件模块:文件模块是在运行过程的时候动态加载的,需要完整的路径,文件定位,编译执行过程,速度也是最慢的。
模块的加载过程
优先从缓存加载
Node对于引入过的模块都会进行缓存,以减少二次引入时的开销,不同于浏览器的是,浏览器仅仅缓存文件,而Node缓存的是编译和执行之后的对象。
无论是核心模块还是文件模块,require()方法对于相同模块的二次加载都是一律采用缓存优先的方式,这是第一优先级的,不同之处在于核心模块的缓存检查优先于文件模块的缓存检查。
路径分析和文件定位
模块标识符分析
核心模块:最快
路径形式的文件模块:较快
自定义模块:最慢
文件定位
文件扩展名分析,按照.js、.node、.json的次序补充扩展名,依次尝试。
PS:建议带上扩展名,以防止性能问题
目录分析和包:首先查找package.json文件。如果不存在该文件,则会去找index.js、index.node、index.json
模块编译
在Node中,每个文件模块都是一个对象。
编译和执行是引入文件模块的最后一个阶段,定位到具体的文件之后,Node会新建一个模块对象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也是不同的。
.js文件,通过fs模块同步读取文件之后编译执行
.node文件,通过用c/c++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
.json文件,通过fs模块同步读取文件之后,使用JSON.parse()方法解析返回结果。
其余扩展文件,都是被当作.js文件载入的。
PS:对于每一次编译成功的模块都会将其文件路径作为索引缓存在Module,_cache对象上,一提高二次引入的性能。
javascript模块的编译
在编译的过程中,Node对获取的javascript文件内容进行了头尾包装。在头部添加了(function(exports,require,module,__filename,__dirname){\n,在尾部添加了\n});
(function(exports,require,module,__filename,__dirname){
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius
}
});
这样每一个模块文件之间都进行了域隔离。
C/C++模块的编译
Node调用process.dlopen()方法进行加载和执行,在Node框架下,dlopen()方法在window和*nix平台下分别有不同的实现,通过libuv兼容层进行封装。
JSON文件的编译
Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法获取到对象,然后将它赋值给模块对象的exports,以供外部调用。
Node核心模块
Node的核心模块分为两块:c/c++编写的+javascript编写的
其中c/c++编写的放置在Node项目的src目录下面,javascript编写的放置在lib目录下
javascript核心模块的编译过程
1.转化为c/c++代码
Node采用了V8附带的js2c.py工具,将所有的内置的javascript代码(src/node.js和lib/*.js)转换成c++里面的数组,生成node_natives.h头文件
在这个过程中,javascript代码以字符串的形式存储在node命名空间里,是不可直接执行的。在启动Node进程的时候,javascript代码直接加载进内存中。在加载的过程中,javascript核心模块经历标识分析直接定位到内存中,这个比普通的文件模块从磁盘中一处一处的查找快的多。
2.编译javascript核心代码
lib目录下面的所有模块文件也没有定义require、module、export这些变量。在进入javascript核心模块的过程中,也经历了头尾包装的过程,然后才执行和导出了exports对象。
与文件模块的区别的地方是:获取源码的方式(核心模块是从内存中直接读取的),以及缓存执行结果的位置。
javascript核心模块的定义如下,源文件是通过process.binding('natives')取出,编译成功的模块缓存到NativeModule._cache对象上,文件模块则缓存到Module._cache对象上;
function NativeModule(id){
this.filename = id + '.js';
this.id = id;
this.exports = {};
this.loaded = false;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
c/c++核心模块的编译过程
我们把那些由纯c/c++编写的部分统一称为内建模块,因为它们通常不被用户直接调用。在Node中,buffer, crypto, evals, fs, os等模块都是部分通过c/c++编写的。
内建模块的组织形式
内建模块的导出
c/c++扩展模块
略,讲的太深了,看不懂,以后再慢慢啃吧!
模块调用栈
略,讲的太深了,看不懂,以后再慢慢啃吧!
包与NPM
包结构
包实际上是一个存档文件,即一个目录直接打包为.zip或者tar.gz格式的文件,安装后解压还原为目录。符合CommonJS规范的包目录应该包含下面的文件
package.json: 包描述文件
bin: 用于存放可执行二进制文件的目录
lib: 用于存放javascript代码的目录
doc: 用于存放 文档的目录
test: 用于存放单元测试用例的代码
包描述文件与NPM
略,讲的太深了,看不懂,以后再慢慢啃吧!
前后端公用模块
略,讲的太深了,看不懂,以后再慢慢啃吧!