本文详细介绍了 Node.js 模块系统,包括了如何引用模块以及定义模块。

什么是模块

模块指的是软件系统里可分离与重用的功能代码。这里的模块与专注于单一功能的函数是一样的,模块是同一主题的函数(和对象)的集合。

比如 String.indexOf() 只是一个提供查找字符串中某个字符串匹配的位置的函数;而 String 对象则是一个封装了多个字符串操作函数及属性的模块

JavaScript 模块

JavaScript 的早期语言规范(ES6 之前)中并没有给出模块系统的概念定义,但一般来说,我们可以把全局对象当作是一个 JavaScript 模块,在浏览器中的 JavaScript 的实现就是这样做的。

但是如果所有的模块都是全局对象(即都放在了一个全局命名空间内),包括你自己定义的模块以及第三方模块,这样难免会发生模块名冲突问题(即定义了同名的全局对象),也称为全局命名空间污染

命名空间在这里指的是所有对象的名称集合。

在 ES6 版本中,JavaScript 也实现了模块系统,关于 ES6 模块系统的详细内容会在另一篇文章里讲解。

Node.js 模块系统

Node.js 定义了一个简单但强大的模块系统,使用该模块系统,你就可以定义不同命名空间下的模块了。

该系统有 3 个核心全局对象

  • require —— 导入当前模块依赖的模块
  • module —— 记录当前模块的元属性
  • exports —— 暴露当前模块的数据或接口

虽说是全局对象,但这个全局并不意味着不同模块间会共享共享这三个对象,恰恰相反,每个模块的这三个对象都是独立的。那么这里的全局实际上是指你可以在单个模块中的任意位置使用这三个对象。

我们先来看看浏览器中 JavaScript 的全局模块。

先创建 a.jsb.js

a.js
value = 'a.js'
b.js
value = 'b.js'

然后在 index.html 中引用这两个 JavaScript 文件。

index.html
<!Doctype html>
<script>
let value = 'index.html'
console.log(value) // index.html
</script>
<script src="a.js"></script>
<script>
console.log(value) // a.js
</script>
<script src="b.js"></script>
<script>
console.log(value) // b.js
</script>

从运行结果可以看出,index.htmla.jsb.js 共享了 value 变量。

而如果我们再创建一个 index.js 文件,并使用 node index.js 运行。

index.js
let value = 'index.js'
console.log(value) // index.js
require('./a.js')
console.log(value) // index.js
require('./b.js')
console.log(value) // index.js

从运行结果可见:index.jsa.jsb.js 并没有共享 value 变量。

如何引入模块

Node.js 中,你可以使用 require 全局函数对象,导入定义好的 Node.js 模块。

require 语法
[ * ] require( [ String ] moduleName )

require 函数接受模块名作为字符串参数,

  • 解析模块的真实路径
  • 加载模块
  • 返回模块定义的导出对象

require 的一般处理流程如下:

require 的一般处理流程

引入 Node 内核模块

const fs = require('fs');
console.log(fs)

以上代码加载了 Node 内核模块 fs,并返回一个对象。

我们将字符串 'fs' 作为参数传递给全局函数 require(),得到了返回结果——一个对象(更具体点说,是一个包含本地文件系统的相关属性与操作的对象)。

那么 require() 是将我们的输入字符串 'fs' 转化成输出对象的呢?

首先,require() 会检查输入的字符串 'fs' 是否匹配预定义的 Node 内核模块名,若匹配,则返回一个 JavaScript 对象。

下面就是 Node.js 8.1.4 内核模块名列表(只列举来稳定版本的模块名):

模块名 模块描述
assert 提供断言测试功能
buffer 提供与八进制流交互功能,可用于网络流和文件系统操作
child_process 提供创建子进程功能
cluster 提供创建集群功能
console 提供调试控制台
crypto 提供加密功能
dgram 提供 UDP 数据包套接字的实现
dns 提供 DNS 域名查找与 DNS 服务器连接
events 提供事件触发与事件侦听功能
fs 提供文件系统 I/O 操作功能
http 提供 HTTP 服务器和客户端
https 提供 HTTPS(HTTP protocol over TLS/SSL) 的实现
module 提供模块加载功能
net 提供创建基于流的 TCP 或 IPC 服务器和客户端功能,使用异步的方式
os 提供操作系统相关实用方法
path 提供与文件与目录的路径相关操作
querystring 提供解析与格式化 URL 查询字符串功能
readline 提供允许从可读流中读取一行数据
repl 提供 Read-Eval-Print-Loop (REPL) 的实现
stream 提供流化数据的抽象接口
string_decoder 提供将 Buffer 对象解码成多字节 UTF-8 和 UTF-16 字符串功能
timer 提供函数的延迟调用功能
tls 提供 Transport Layer Security (TLS) 和 Secure Socket Layer (SSL) 协议的实现
tty 提供与 text terminal (“TTY”) 相关功能
url 提供 URL 解析功能
util 提供实用方法
vm 提供编译与运行代码
zlib 提供压缩功能

详情可见在 Node.js 在 GitHub 的源码

需要注意的是,这些内核模块都是 Node 内部实现的,即使用了 C++ 代码来实现以上功能。

引入相对或绝对模块

Node.js 除了提供系统基础的内核模块外,还允许开发者定义自己的模块,并可以在 JavaScript 文件中导入该模块。

Node.js 允许以下方式定义模块:

  • JavaScript 文件,可包含 ‘.js’ 后缀名或不包含 ‘.js’ 后缀名
  • JSON 文件,必须包含 ‘.json’ 后缀名
  • C++ 编译的二进制插件,必须包含 ‘.node’ 后缀名

Node.js 允许我们使用以下语法引用上述模块:

require('/${module_name}')
require('./${module_name}')
require('../${module_name}')

${module_name} 为任意的模块名。

其中,以 / 开头的为绝对模块,以 ./hello../hello 开头的为相对模块。

当我们使用以下语句加载 hello 模块时:

require('./hello')

字符串 './hello''./' 开头,那么 hello 模块将当作相对模块,然后解析其绝对路径。
假设当前目录 (即 '.') 是 /User/sysyiya/dev/,那么 Node.js 首先会把 hello 模块当作文件模块,依次判断以下文件是否存在:

  1. /User/sysyiya/dev/hello
  2. /User/sysyiya/dev/hello.js
  3. /User/sysyiya/dev/hello.json
  4. /User/sysyiya/dev/hello.node

Node.js 会依次查找以上文件,当找到符合要求的文件,就不再继续向下寻找。

如果以上四个文件都没有找到,Node.js 会尝试把 hello 模块当作目录模块,查找 /User/sysyiya/dev/hello/ 目录是否存在。

如果目录 /User/sysyiya/dev/hello/ 存在,那么就查找该目录下的 /User/sysyiya/dev/hello/package.json 是否存在。
如果 JSON 文件 /User/sysyiya/dev/hello/package.json 不存在,或者文件存在,但没有设置 main 属性,那么 Node.js 将在 /User/sysyiya/dev/hello/ 目录下查找 index 文件模块

如果 JSON 文件 /User/sysyiya/dev/hello/package.json 存在,并且已设置 main 属性。
假设 /User/sysyiya/dev/hello/package.json 内容如下:

/User/sysyiya/dev/hello/package.json
{
"main": "main"
}

那么 Node.js 将会依次查找以下文件是否存在:

  1. /User/sysyiya/dev/hello/main
  2. /User/sysyiya/dev/hello/main.js
  3. /User/sysyiya/dev/hello/main.json
  4. /User/sysyiya/dev/hello/main.node

如果以上四个文件都不存在,又会查找 /User/sysyiya/dev/hello/main/ 目录,一直重复以上过程,直到找到文件,或者找不到对应目录为止。

相对或绝对模块查找流程图

导入 node_modules 模块

Node.js 还允许一种更简单的查找模块的方式,就是用使用 node_modules 模块

通过创建一个 node_modules 目录,将文件模块或者目录模块放在该目录下,便可以使用与调用 Node.js 内核模块一样简洁的方式来调用自定义模块。

假设 node_modules 目录结构如下:

node_modules
├── a.js
├── b/
│ └── index.js
└── c/
├── c.js
└── package.json

以上文件内容分别是

a.js
exports.id = 'a.js'
b/index.js
exports.id = 'b.js'
c/package.json
{
"main": "c"
}
c/c.js
exports.id = 'c.js'

则可以通过以下命令调用 abc 三个模块,并打印返回对象:

$ node -e 'console.log(require("a"))'
{ id: 'a.js' }
$ node -e 'console.log(require("b"))'
{ id: 'b.js' }
$ node -e 'console.log(require("c"))'
{ id: 'c.js' }

需要记住的一个关键点就是 node_modules 的位置,当你使用 require() 导入 node_modules 中的模块时,node_modules 应该位于以下位置:

  • 当前目录下
  • 当前目录的多级上级目录下
  • 全局目录下

假设在 /Users/sysiya/dev/playgroud/index.js 中使用 require('a') 引入 a 模块,那么 Node.js 将会依次在以下目录中搜索 a 模块:

  1. /Users/sysiya/dev/playgroud/node_modules/
  2. /Users/sysiya/dev/node_modules/
  3. /Users/sysiya/node_modules/
  4. /Users/node_modules/
  5. /node_modules/
  6. /usr/local/lib/node_modules/

1 ~ 5 分别是搜索从当前目录到根目录下的 node_modules
6 是搜索全局目录,在作者的 macOS 10.12.6 上,该目录为 /usr/local/lib/node_modules/

还有一种特殊情况需要注意,如果当前目录的路径就是以 node_modules 结尾时,不会在当前目录的 node_modules 下寻找模块。

/Users/sysiya/dev/playgroud/node_modules/index.js 的查询顺序为:

  1. /Users/sysiya/dev/playgroud/node_modules/
  2. /Users/sysiya/dev/node_modules/
  3. /Users/sysiya/node_modules/
  4. /Users/node_modules/
  5. /node_modules/
  6. /usr/local/lib/node_modules/

注意第一次查询的是 /Users/sysiya/dev/playgroud/node_modules/,而不是 /Users/sysiya/dev/playgroud/node_modules/node_modules/

定义模块

之前就提过,Node.js 默认有三种方式创建模块:

  • JavaScript 文件
  • JSON 文件
  • Node 二进制文件(C++ 编译)

实际上,你可以通过以下命令查看 Node.js 支持的扩展名。

$ node -e 'console.log(require.extensions)'
{ '.js': [Function], '.json': [Function], '.node': [Function] }

当你提供任何其他扩展名或者没有扩展名,Node.js 都会将其按照 JavaScript 文件内容解析。

JavaScript

使用 JavaScript 定义模块时,使用 module.exports 暴露导出对象。

导出对象

// 默认导出 {}
module.exports = {}
// 导出自定义对象
module.exports = {
name: 'a',
say: function () {
return 'hello'
},
eat () {
return 'food'
},
do: () => {
return 'somthing'
}
}

导出函数

// 导出函数
module.exports = function () {
return 'function'
}
// 导出箭头函数
module.exports = () => {
return 'arrow'
}

JSON

{
"name": "sysiya",
"age": 21
}

使用 require() 导入 JSON 对象,有以下限制:

  • 不能导出函数
  • 导出的对象中也不能包括函数

C++ 插件

1. 创建 C++ 文件 hello.cc

hello.cc
#include <node.h>
namespace demo {
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;
void Method(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
args.GetReturnValue().Set(String::NewFromUtf8(isolate, "world"));
}
void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "hello", Method);
}
NODE_MODULE(addon, init)
} // namespace demo

2. 创建构建配置文件 binding.gyp

binding.gyp
{
"targets": [
{
"target_name": "addon",
"sources": [ "hello.cc" ]
}
]
}

3. 生成配置信息

$ node-gyp configure

首先需要安装 node-gyp

> $ npm install -g node-gyp
>

命令执行成功会生成 build 文件夹

4. 构建 .node 二进制文件

$ node-gyp build

生成 build/Release/addon.node