nodejs之模块加载

本文介绍 nodejs 中的模块加载方式及用法。

前言

在 Node.js 模块系统中,每个文件都被视为一个独立的模块。 例如,假设有一个名为 foo.js 的文件:

1
2
const circle = require('./circle.js');
console.log(`半径为 4 的圆的面积是 ${circle.area(4)}`);

在第一行中, foo.js 加载了与 foo.js 在同一目录中的 circle.js 模块。

以下是 circle.js 的内容:

1
2
3
4
5
const { PI } = Math;

exports.area = (r) => PI * r ** 2;

exports.circumference = (r) => 2 * PI * r;

circle.js 模块导出了 area() 和 circumference() 函数。 通过在特殊的 exports 对象上指定额外的属性,可以将函数和对象添加到模块的根部。

模块内的本地变量是私有的,因为模块由 Node.js 封装在一个函数中(详见模块封装器)。 在这个例子中,变量 PI 对 circle.js 是私有的。

可以为 module.exports 属性分配新的值(例如函数或对象)。

下面的例子中, bar.js 使用了导出 Square 类的 square 模块:

1
2
3
const Square = require('./square.js');
const mySquare = new Square(2);
console.log(`mySquare 的面积是 ${mySquare.area()}`);

square 模块定义在 square.js 中:

1
2
3
4
5
6
7
8
9
10
// 赋值给 `exports` 不会修改模块,必须使用 `module.exports`。
module.exports = class Square {
constructor(width) {
this.width = width;
}

area() {
return this.width ** 2;
}
};

模块系统在 require(‘module’) 模块中实现。

模块定义

nodejs 中,一个模块的定义或者说导出方式是使用 module.exports 或者 exports

1
2
3
4
5
6
7
8
// foo.js
module.exports = {
name: 'foo'
say(){
console.log('this is '+ this.name +' module')
}
// ...
}

如上,我们定义了 foo.js 模块,你可以是使用 require 函数来引用她。

使用 exports 向外提供接口。

1
2
3
4
// baz.js
exports.say = function( text ){
console.log( 'hello' + text + '!' )
}

如上,baz 模块向外提供了 say 方法。

模块导入

nodejs 中,使用 require 函数来引用一个模块。

1
2
3
// bar.js
const foo = require('./foo.js')
foo.say() // this is foo module

如上代码,我们引用了 foo.js 模块,并使用了 foo.say 方法。

exports 和 module.exports

我们已经知道了在 nodejs 中 模块的定义及引用方式,那么 exportsmodule.exports 这两种导出方式有何不同呢? 当你使用 require 函数引用一个模块时,她返回的永远是 module.exports 对象。

类似于对 exports 重新赋值对外导出接口的方式是无效的:

1
2
3
4
5
// foo.js
exports = {
msg: 'wow'
// ...
}
1
2
3
4
// bar.js
const foo = require('./foo.js')
console.log( foo.msg ) // msg is undefined
console.log( foo ) // {}

如上代码的导出方式可以简单的这样理解:

1
2
3
4
5
6
7
8
var a = {}
var b = a;
b = {
msg: 'wow'
}

console.log( a ) // {}

所以,你可以为 exports 快捷方式添加属性,但是不可以对其直接重新赋值。

nodejs 中的模块加载规则

模块类型

nodejs 中,存在不同类型的模块:外部模块,核心模块,文件模块,文件夹模块。那么对于这些模块的加载规则都有所不同。

外部模块

所谓 外部模块 指的是使用 npm 管理的第三方模块,即安装在 node_modules 文件夹中的模块。比如:koa等。

核心模块

核心模块 指的是 nodejs 自身提供的一些内部模块,无需安装,可直接 require 引用。比如:http, path, fs 等。

文件模块

在本地自己新建的 *.js*.json*.node 被称作 文件模块

目录模块

根据模块加载规则,nodejs 中,我们认为一个文件夹也是一个模块,也就是说,我们可以 require('./directory') 来引用一个模块。

1
2
3
// foo 是一个文件夹,该文件夹下存在一个  index.js 文件
const foo = require('./foo')
// do something...

具体为什么可以这么引用,在接下来的模块加载规则中会详细说明。

加载规则

先看如下伪代码描述的高级算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with '/'
a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
4. LOAD_NODE_MODULES(X, dirname(Y))
5. THROW "not found"

LOAD_AS_FILE(X)
1. If X is a file, load X as JavaScript text. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP

LOAD_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.node as binary addon. STOP

LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and look for "main" field.
b. If "main" is a falsy value, GOTO 2.
c. let M = X + (json main field)
d. LOAD_AS_FILE(M)
e. LOAD_INDEX(M)
f. LOAD_INDEX(X) DEPRECATED
g. THROW "not found"
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 = [GLOBAL_FOLDERS]
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

翻译过来的具体行为如下:

假设你要引用一个模块 x (x 可以是 nodejs 的核心模块,外部第三方模块,文件模块或者一个目录)。

1
const x = require(x)
  1. 判断模块 x 是否为 nodejs 核心模块,如果是,直接返回该核心模块

  2. 不满足1时,判断 x 是否是以 /./../ 开头,如果是,这 x 被认为是 本地模块(文件模块或目录模块)

  3. 如果根据路径找到的是文件类型 *.js、*.json、*.node ,则根据扩展名解析返回为不同的数据。

  4. 如果是 x.js 文件,则返回 javascript 文本。

  5. 如果是 x.json 文件,则将其解析为 javascript 对象并返回。

  6. 如果是 x.node 文件,则将其作为 node 的二进制插件并返回。

  7. 如果根据路径找到的是文件夹类型,那么 node 会继续在该文件夹下查找以 index.* 的文件,包括 index.jsindex.jsonindex.node等文件并将其返回。像在使用 webpack 这样的构建系统的 @vue/cli 项目中,index.vue 也会被匹配到。

  8. 如果根据以上条件都没有匹配到模块文件,那么 node 会将 x 视为 第三方外部模块

  9. 满足条件 8 时,node 会以当前引用 x 模块的文件为原点,递归向外查找 node_modules 文件夹,直到项目根目录。

  10. 只要找到了第一个 node_modules 文件夹,那么 node 便会停止递归查找,并在该 node_modules 文件夹下查找 x 文件夹。

  11. 如果在 node_modules 中没有找到 x 文件夹,那么抛出 not found 错误。

  12. 如果在 node_modules 找到了 x 文件夹,那么 node 会查看解析 x 文件夹下的 package.json 文件

  13. 再查看该 package.json 文件中是否存在 main 字段,该字段表示 x 模块的入口文件路径,node 会根据该文件路径解析出具体的文件并将其返回。

  14. 如果 package.json 文件都不存在,那么 node 会尝试查找 x 文件夹下是否存在一个 index.js 的文件,如果有,直接将其返回,如果没有抛出 not found 错误。

  15. 至此,如果根据以上规则都没有找到指定的模块,那么 node 就不会再查找了,直接抛出 not found 错误。

模块封装器

在执行模块代码之前,Node.js 会使用一个如下的函数封装器将其封装:

1
2
3
(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});

通过这样做,Node.js 实现了以下几点:

它保持了顶层的变量(用 var、 const 或 let 定义)作用在模块范围内,而不是全局对象。

它有助于提供一些看似全局的但实际上是模块特定的变量,例如:

实现者可以用于从模块中导出值的 moduleexports 对象。
包含模块绝对文件名和目录路径的快捷变量 __filename__dirname