Node.js模块系统
Node.js有一个简单的模块加载系统。 在Node.js中,文件和模块是一一对应的(每个文件被视为单独的模块)。
例如,考虑下面这个名为foo.js
的文件:
const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);
在第一行,foo.js
加载与foo.js
同一目录的模块circle.js
。
circle.js
的内容如下:
const PI = Math.PI;
exports.area = (r) => PI * r * r;
exports.circumference = (r) => 2* PI * r;
模块circle.js
向外部分别暴露了函数area()
和circumference()
。 所以如果要将函数或者对象向外暴露出去,可以将它们赋值到一个特殊exports
对象上。
使用exports
和module.exports
的区别
var module = {
exports: {}
};
// 1:给 exports 添加字段,并给该字段赋值
(function(module, exports) {
exports.a = 1;
})(module, module.exports);
console.log(module.exports.a); // => 1
console.log(JSON.stringify(module)); // => {"exports":{"a":1}}
// 2:将 exports 覆盖并赋值
(function(module, exports) {
exports = {
b: 1
};
})(module, module.exports);
console.log(module.exports.b); // => undefined
console.log(JSON.stringify(module)); // => {"exports":{"a":1}}
// 3:分别给module.exports 和 exports 添加字段并给该字段赋值
(function(module, exports) {
module.exports.c = 'first C';
exports.c = 'second C';
})(module, module.exports);
console.log(module.exports.c); // => second C
console.log(JSON.stringify(module)); // => {"exports":{"a":1,"c":"second C"}}
// 4:给module.exports赋值, exports 添加字段并赋值
(function(module, exports) {
module.exports = {d:'first D'};
exports.d = 'second D';
})(module, module.exports);
console.log(module.exports.d); // => first D
console.log(JSON.stringify(module)); // => {"exports":{"d":"first D"}}
内部变量
模块内部的变量最终一定是私有的,因为模块的内容会被Node.js包裹在一个函数中(参见下面的模块包装器)。 在上面的那个例子中,变量PI
对于circle.js
来说是私有变量,外部是获取不到这个变量的。
如果你希望模块向外暴露的是一个函数(如构造函数),或者是一个完整的对象。则需要将其分配给module.exports
而不是exports
。 (原因请参考上面的示例代码)
在下面的bar.js
中,使用了square
模块,它导出一个函数:
const square = require('./square.js');
var mySquare = square(2);
console.log(`The area of my square is ${mySquare.area()}`);
在square.js
模块中定义一个square
方法:
module.exports = (width) => {
return {
area: () => width * width;
};
}
此外,模块系统在
require(“module”)
模块中实现。
『main』模块
当某个module
直接从Node.js运行时,它会将require.main
设置该module
。 你可以通过这个来测试这个module
是被直接运行的还是被require
的。
require.main === module
就拿文件foo.js
来说,如果运行node foo.js
这个属性就是true
。运行require('./foo.js')
就是false
。
因为module
提供了一个filename
(通常相当于__filename
),因此可以通过检查require.main.filename
来获取当前应用程序的入口点。
包管理器的一些提示
Node.js的require()
函数支持一些合理的目录结构。它让软件包管理器程序(如dpkg
,rpm
和npm
)可以从Node.js模块中直接去构建本地的包而不需要修改。
下面我们给出一个可以正常工作的建议目录结构:
假设我们希望在/usr/lib/node/<some-package>/<some-version>
中的文件夹来指定版本的包。
此外,包还可以相互依赖。 比如你想安装foo
包,而这个包有可能需要安装指定版本的bar
包。而bar
包也很有可能依赖其他的包,并且在某些特殊情况下,这些依赖包甚至可能会产生循环依赖。
由于Node.js会查找加载的所有模块的realpath
(即解析软链),然后再去node_modules文件夹中查找依赖的包,因此使用以下方案可以非常简单地解决此问题:
/usr/lib/node/foo/1.2.3/
- 包含foo
包,版本是1.2.3
/usr/lib/node/bar/4.3.2/
- 包含foo
所依赖的bar
包/usr/lib/node/foo/1.2.3/node_modules/bar
- 软链到/usr/lib/node/bar/4.3.2/
/usr/lib/node/bar/4.3.2/node_modules/*
- 软链到bar
的依赖
因此,即使遇到循环依赖,或者是依赖冲突,每个模块都能加载到并使用自己所依赖指定版本的包。
当foo
包中require('bar')
时,它就可以软链到指定版本的/usr/lib/node/foo/1.2.3/node_modules/bar
。然后,当bar
包中的代码调用require('quux')
时,它同样也可以软链到指定版本的/usr/lib/node/bar/4.3.2/node_modules/quux
。
模块加载的全过程(重点,下面写的伪代码流程一定要记住)
要获取在调用require()
将被加载的确切文件名,请使用require.resolve()
函数。
以下是模块加载的全过程以及require.resolve
的解析过程:
// 加载X模块
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 './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"
// 加载X文件
// 加载过程:X -> X.js -> X.json -> X.node
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, load [X.json] as JavaScript text. STOP
4. If [X.node] is a file, load [X.node] as JavaScript text. STOP
// 加载入口文件
// 加载过程:X -> X/index.js -> X/index.json -> X/index.node
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, load [X/index.json] as JavaScript text. STOP
3. If [X/index.node] if a file, load [X/index.node] as JavaScript text. STOP
// 加载文件夹
LOAD_AS_DIRECTORY(X)
1. If [X/package.json] is a file.
a. Parse [X/package.json], and look for "main" field
b. let M = X + (json main field)
c. LOAD_AS_FILE(M)
d. LOAD_INDEX(M)
2. LOAD_INDEX(X)
// 加载node模块
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路径
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
模块缓存
所有的模块都会在第一次加载之后被缓存起来。 这意味着你每次调用require('foo')
将得到完全相同的对象。
对require('foo')
的多次调用可能并不会多次执行该模块的代码。 这是一个重要的功能。 使用它,可以返回“partially done”对象,从而允许根据依赖关系一层一层地加载模块,即使这样做可能会导致循环依赖。
如果要让某个模块在每次被加载时都去执行代码,则需要
exports
一个函数,并调用该函数即可。
模块缓存注意事项
模块是基于其解析出来的文件名进行缓存。根据调用模块的路径,被调用的模块可能会解析出不同的文件名(从node_modules文件夹加载)。如果解析出来的是不同的文件,它不保证每次require('foo')
总是返回相同的对象。
另外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向相同的文件,但缓存仍将它们视为不同的模块,并将重新加载该文件多次。 例如,require('./ foo')
和require('./ FOO')
返回两个不同的对象,而不管./foo
和./FOO
是否是同一个文件。
核心模块
Node.js有些模块被编译成二进制文件。 本文档中的其他部分将对这些模块进行更详细的描述。
核心模块在Node.js的源码lib/
文件夹中。
如果核心模块的模块标识传递给require()
,则它们总是优先加载。 例如,即使有一个自定义模块叫http
,我们去执行require('http')
也将始终返回内置的HTTP
模块,
循环引用
当循环引用require()
时,返回模块可能并没有执行完成。
考虑这种情况:
a.js
:
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
:
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
app.js
:
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
当app.js
加载a.js
时,a.js
依次加载b.js
. 此时,b.js
尝试加载a.js
. 为了防止无限循环,将a.js
导出对象的未完成副本返回到b.js
模块。 b.js
然后完成加载,并将其导出对象提供给a.js
模块。
当app.js
加载了这两个模块时,它们都已经完成。 因此,该程序的输出将是:
$ node app.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
in main, a.done =true, b.done = true
模块包装器
在执行模块的代码之前,Node.js将使用一个函数包装器来将模块内容包裹起来,如下所示:
(function (exports, require, module, __filename, __dirname) {
// 你的模块代码
});
通过这样做,Node.js实现了以下几点:
它将模块内部的顶级变量(定义为
var
,const
或let
)的作用域范围限定为模块内部而不是全局。-
它有助于给模块内部提供一些实际上只属于该模块的全局变量,例如:
module
和exports
对象用来帮助从模块内部导出一些值变量
__filename
和__dirname
是当前模块最终解析出来的文件名和文件夹路径
module
对象签名
Object module {
id: String, // 模块标识,为该模块文件在系统中的绝对路径
exports: Object, // 该模块的导出对象
parent: Object | undefined, // 引用该模块的父模块
filename: String | null, // 最终解析的文件名称, 与__filename相同。
loaded: Boolean, // 该模块是否已经加载
children: Array, // 改模块的引用列表
paths: Array // 模块加载路径
}
require
函数签名
Function require {
[Function], // 函数体
resolve: Function, // 根据模块标识解析模块,返回绝对路径
main: undefined | Object, // 应用的主(main)模块
extensions: {'.js':Function, '.json':Function, '.node':Function},
cache: Object // 模块缓存,以模块的绝对路径为key
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。