12、Node.js 模块系统

Node.js 提供了模块管理机制,使得文件之间可以相互包含和调用

模块是Node.js 应用程序的基本组成部分

Node.js 文件和模块是一一对应的

一个Node.js 文件就是一个模块,这个文件可以是 JavaScript 代码、JSON 或者编译过的C/C++ 扩展

创建模块

Node.js 中创建模块非常简单,因为模块可以是一个以 .js 为扩展名的 JavaScript 模块

比如我们定义一个 hello 模块,只需要新建 hello.js

var hello = require('./hello');
hello.world();

以上实例中,代码 require(‘./hello’) 引入了当前目录下的 hello.js 文件(./ 为当前目录,node.js 默认后缀为 js)。

Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。

接下来我们就来创建 hello.js 文件,代码如下:

exports.world = function() {
  console.log('Hello World');
}

在以上示例中,hello.js 通过 exports 对象把 world 作为模块的访问接口,在 main.js 中通过 require(‘./hello’) 加载这个模块,然后就可以直接访 问 hello.js 中 exports 对象的成员函数了。

有时候我们只是想把一个对象封装到模块中,格式如下:

module.exports = function() {
  // ...
}

例如:

//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'); 
hello = new Hello(); 
hello.setName('BYVoid'); 
hello.sayHello();

模块接口的唯一变化是使用 module.exports = Hello 代替了exports.world = function(){}。 在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的 exports。


服务端的模块放在哪里

require() 方法可以把一个模块加载进当前文件

就像下面这样

main.js

/*
 * filename: main.js
 * author: DDKK.COM 弟弟快看,程序员编程资料站(ddkk.com)
 * Copyright © 2015-2065 ddkk.com. All rights reserved.
*/

var http = require("http");

...

http.createServer(...);

上面的代码,我们加载了 Node.js 自带的 http 模块,并把这个模块赋值给 http 变量

这样我们的 http 变量就拥有了 http 模块所有的公共方法、对象和属性

require() 查找模块策略

那么,Node.js 中的 require() 方法是怎么查找一个模块的呢?

Node.js 中存在 4 种模块: 官方自带的模块 和 其它三种文件模块

虽然调用 require 方法非常简单,但 require 内部实现却非常复杂,而且 4 种类型的模块加载优先级也各不相同

我们用一张图来演示 require 加载模块的机制

 

从模块缓存中加载

我们可以从上图中看到,虽然 Node.js 官方自带的模块和文件模块的优先级是不一样的,但都会优先从模块缓存中加载已经存在的模块

加载 Node.js 官方自带模块

原生模块的优先级仅次于模块缓存的优先级

require 方法在解析文件名之后,优先检查模块是否在原生模块列表中

我们以加载 http 模块为例

尽管当前目录下可能存在 “http” 、http.js 、http.node 、http.json 文件 require(“http”) 都不会从这些文件中加载,而是从原生模块中加载

原生模块也有一个缓存区,同样也是优先从缓存区加载

如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行

从文件加载

当一个模块在模块缓存中不存在,而且又不是原生模块的时候,Node.js 会解析 require 方法传入的参数,并从文件系统中加载实际的文件

我们在前面的章节中已经学习了模块加载过程中的包装和编译细节,这里我们将主要讲解查找文件模块的过程

require() 方法接收一下几种类型的模块参数名

1、 http、fs、path等原生模块;
2、 ./mod或../mod等相对路径的文件模块;
3、 /path_to_module/mod这种绝对路径的文件模块;
4、 mod只提供模块名的非原生模块的文件模块;

下面是在路径 Y 下执行 require(X) 语句执行顺序

1、 如果 X 是内置模块
   a. 返回内置模块
   b. 停止执行

2、 如果 X 以 '/' 开头
   a. 设置 Y 为文件根路径

3、 如果 X 以 './' 或 '/' or '../' 开头
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)

4、 LOAD_NODE_MODULES(X, dirname(Y))

5、 抛出异常 "not found"


LOAD_AS_FILE(X)
1、 如果 X 是一个文件, 将 X 作为 JavaScript 文本载入并停止执行。
2、 如果 X.js 是一个文件, 将 X.js 作为 JavaScript 文本载入并停止执行。
3、 如果 X.json 是一个文件, 解析 X.json 为 JavaScript 对象并停止执行。
4、 如果 X.node 是一个文件, 将 X.node 作为二进制插件载入并停止执行。


LOAD_INDEX(X)
1、 如果 X/index.js 是一个文件,  将 X/index.js 作为 JavaScript 文本载入并停止执行。
2、 如果 X/index.json 是一个文件, 解析 X/index.json 为 JavaScript 对象并停止执行。
3、 如果 X/index.node 是一个文件,  将 X/index.node 作为二进制插件载入并停止执行。


LOAD_AS_DIRECTORY(X)
1、 如果 X/package.json 是一个文件,
   a. 解析 X/package.json, 并查找 "main" 字段。
   b. let M = X + (json main 字段)
   c. LOAD_AS_FILE(M)
   d. LOAD_INDEX(M)
2、 LOAD_INDEX(X)

LOAD_NODE_MODULES(X, START)
1、 let DIRS=NODE_MODULES_PATHS(START)
2、 for each DIR in DIRS:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1、 let PARTS = path split(START)
2、 let I = count of PARTS - 1
3、 let DIRS = []
4、 while I >= 0,
   a. if PARTS[I] = "node_modules" CONTINUE
   b. DIR = path join(PARTS[0 .. I] + "node_modules")
   c. DIRS = DIRS + DIR
   d. let I = I - 1
5、 return DIRS