在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文件载入。
每一个编译成功的模块都会将其文件路径作为索引缓存在`Module._cache`对象上,以提高二次引入的性能。
根据不同的文件扩展名,Node会调用不同的读取方式,如.json文件的调用如下:
~~~
//Native extension for .json
Module._extensions['.json'] = function(module, filename){
var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
try{
module.exports = JSON.parse(stripBOM(content));
} catch(err){
err.message = filename + ': ' + err.message;
throw err;
}
};
~~~
其中,Module.extensions会被赋值给require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。编写如下代码测试一下:
~~~
console.log(require.extensions);
~~~
得到的执行结果如下:
~~~
{ '.js': [Function], '.json': [Function], '.node': [Function] }
~~~
如果想对自定义的扩展名进行特殊的加载,可以通过类似require.extensions['.ext']的方式实现。早起的CoffeeScript文件就是通过添加require.extensions['.coffee']扩展的方式来实现加载的。但是从v0.10.6版本开始,官方不鼓励通过这种方式来进行自定义扩展名的加载,而是期望先将其它语言或文件编译成JavaScript文件后再加载,这样做的好处在于不将繁琐的编译加载等过程引入Node的执行过程中。
在确定文件的扩展名之后,Node将调用具体的编译方式来将文件执行后返回给调用者。
## 1.JavaScript模块的编译
回到CommonJS模块规范,我们知道每个模块文件中存在着require、exports、module这三个变量,但是它们在模块文件中并没有定义,那么从何而来呢?甚至在Node的API文档中,我们知道每个模块还有`__filename`、`__dirname`这两个变量的存在,它们又是从何而来呢?如果我们把直接定义模块的过程放诸在浏览器端,会存在污染全局变量的情况。
事实上,在编译过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了`(function(exports,require,module,__filename,__dirname){\n`,在尾部添加了`\n});`。
一个正常的JavaScript会被包装成如下的样子:
~~~
(function(exports, require,module,__filename,__dirname){
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius;
};
});
~~~
这样每个模块文件之间都进行了作用于隔离。包装之后的代码会通过vm原生模块的runInThisContex() 方法执行(类似eval,只是具有明确上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。
这就是这些变量并没有定义在每个模块文件中却存在的原因。在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用。
至此,require、exports、module的流程已经完整,这就是Node对CommonJS规范的实现。
此外,许多初学者都曾经纠结过为何存在exports的情况下,还有module.exports。理想情况下,只要赋值给exports即可:
~~~
exports = function(){
//my class
};
~~~
但是通常都会得到一个失败的结果。其原因在于,exports对象是通过形参的方式传入的,直接赋值形参会改变形参的引用,但并不能改变作用域外的值。测试代码如下:
~~~
var chage = (a)=>{
a=100;
console.log(a); // => 100
};
var a = 10;
chage(a);
console.log(a); // => 10
~~~
如果要达到require引入一个类的效果,请赋值给module.exports对象。这个迂回的方案不改变形参的引用。
## 2.C/C++模块的编译
Node调用process.dlopen()方法进行加载和执行。在Node的架构下,dlopen()方法在Windows和 *nix 平台下分别有不同的实现,通过 libuv兼容层进行了封装。
实际上,.node的模块文件并不需要编译,因为它是编写C/C++模块之后编译生成的,所以这里只有加载和执行的过程。在执行的过程中,模块的exports对象与.node模块产生联系,然后返回给调用者。
C/C++模块给Node使用者带来的优势主要是执行效率方面的,劣势则是C/C++模块的编写门槛比JavaScript高。
## 3.JSON文件的编译
.json文件的编译是3种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后将它赋给模块对象的exports,以供外部调用。
JSON文件在用作项目的配置文件时比较有用。如果你定义了一个JSON文件作为配置,那就不必调用fs模块去异步读取和解析,直接调用require()引入即可。此外,你还可以享受到模块缓存的便利,并且二次引入时也没有性能影响。
这里我们提到的模块编译都是指文件模块,即用户自己编写的模块。在下一节中,我们将展开介绍核心模块中的JavaScript模块和C/C++模块。
- 目录
- 第1章 Node 简介
- 1.1 Node 的诞生历程
- 1.2 Node 的命名与起源
- 1.2.1 为什么是 JavaScript
- 1.2.2 为什么叫 Node
- 1.3 Node给JavaScript带来的意义
- 1.4 Node 的特点
- 1.4.1 异步 I/O
- 1.4.2 事件与回调函数
- 1.4.3 单线程
- 1.4.4 跨平台
- 1.5 Node 的应用场景
- 1.5.1 I/O 密集型
- 1.5.2 是否不擅长CPU密集型业务
- 1.5.3 与遗留系统和平共处
- 1.5.4 分布式应用
- 1.6 Node 的使用者
- 1.7 参考资源
- 第2章 模块机制
- 2.1 CommonJS 规范
- 2.1.1 CommonJS 的出发点
- 2.1.2 CommonJS 的模块规范
- 2.2 Node 的模块实现
- 2.2.1 优先从缓存加载
- 2.2.2 路径分析和文件定位
- 2.2.3 模块编译
- 2.3 核心模块
- 2.3.1 JavaScript核心模块的编译过程
- 2.3.2 C/C++核心模块的编译过程
- 2.3.3 核心模块的引入流程
- 2.3.4 编写核心模块
- 2.4 C/C++扩展模块
- 2.4.1 前提条件
- 2.4.2 C/C++扩展模块的编写
- 2.4.3 C/C++扩展模块的编译
- 2.4.2 C/C++扩展模块的加载
- 2.5 模块调用栈
- 2.6 包与NPM
- 2.6.1 包结构
- 2.6.2 包描述文件与NPM
- 2.6.3 NPM常用功能
- 2.6.4 局域NPM
- 2.6.5 NPM潜在问题
- 2.7 前后端共用模块
- 2.7.1 模块的侧重点
- 2.7.2 AMD规范
- 2.7.3 CMD规范
- 2.7.4 兼容多种模块规范
- 2.8 总结
- 2.9 参考资源
- 第3章 异步I/O
- 3.1 为什么要异步I/O
- 3.1.1 用户体验
- 3.1.2 资源分配
- 3.2 异步I/O实现现状
- 3.2.1 异步I/O与非阻塞I/O
- 3.2.2 理想的非阻塞异步I/O
- 3.2.3 现实的异步I/O
- 3.3 Node的异步I/O
- 3.3.1 事件循环
- 3.3.2 观察者
- 3.3.3 请求对象
- 3.3.4 执行回调
- 3.3.5 小结
- 3.4 非I/O的异步API
- 3.4.1 定时器
- 3.5 事件驱动与高性能服务器