ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
# Modules ~~~ 稳定度: 5 - 已锁定 ~~~ Node有一个简易的模块加载系统。在node中,文件和模块是一一对应的。下面示例是`foo.js`加载同一目录下的`circle.js`。 `foo.js`的内容: ~~~ var circle = require('./circle.js'); console.log( 'The area of a circle of radius 4 is ' + circle.area(4)); ~~~ `circle.js`的内容: ~~~ var PI = Math.PI; exports.area = function (r) { return PI * r * r; }; exports.circumference = function (r) { return 2 * PI * r; }; ~~~ `circle.js`模块输出了`area()`和`circumference()`两个函数。要输出某个对象,把它加到`exports`这个特殊对象下即可。 注意,`exports`是`module.exports`的一个引用,只是为了用起来方便。当你想输出的是例如构造函数这样的单个项目,那么需要使用`module.exports`。 ~~~ // 正确输出构造函数 module.exports = MyConstructor; ~~~ 模块内的本地变量是私有的。在这里例子中,`PI`这个变量就是`circle.js`私有的。 模块系统的实现在`require("module")`中。 ### 循环 当存在循环的`require()`调用时,一个模块可能在返回时并不会被执行。 考虑这样一种情形: `a.js`: ~~~ console.log('a starting'); exports.done = false; var 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; var a = require('./a.js'); console.log('in b, a.done = %j', a.done); exports.done = true; console.log('b done'); ~~~ `main.js`: ~~~ console.log('main starting'); var a = require('./a.js'); var b = require('./b.js'); console.log('in main, a.done=%j, b.done=%j', a.done, b.done); ~~~ 首先`main.js`加载`a.js`,接着`a.js`又去加载`b.js`。这时,`b.js`会尝试去加载`a.js`。为了防止无限的循环,`a.js`会返回一个**unfinished copy**给`b.js`。然后`b.js`就会停止加载,并将其`exports`对象返回给`a.js`模块。 这样`main.js`就把这两个模块都加载完成了。这段程序的输出如下: ~~~ $ node main.js main starting a starting b starting in b, a.done = false b done in a, b.done = true a done in main, a.done=true, b.done=true ~~~ 如果你的程序中有循环的模块依赖,请确保工作正常。 ### 核心模块 Node中有一些模块是编译成二进制的。这些模块在本文档的其他地方有更详细的描述。 核心模块定义在node源代码的`lib/`目录下。 `require()`总是会优先加载核心模块。例如,`require('http')`总是返回编译好的HTTP模块,而不管是否有这个名字的文件。 ### 文件模块 如果按文件名没有查找到,那么node会添加 `.js`和 `.json`后缀名,再尝试加载,如果还是没有找到,最后会加上`.node`的后缀名再次尝试加载。 `.js` 会被解析为Javascript纯文本文件,`.json` 会被解析为JSON格式的纯文本文件. `.node` 则会被解析为编译后的插件模块,由`dlopen`进行加载。 模块以`'/'`为前缀,则表示绝对路径。例如,`require('/home/marco/foo.js')` ,加载的是`/home/marco/foo.js`这个文件。 模块以`'./'`为前缀,则路径是相对于调用`require()`的文件。 也就是说,`circle.js`必须和`foo.js`在同一目录下,`require('./circle')`才能找到。 当没有以'/'或者'./'来指向一个文件时,这个模块要么是"核心模块",要么就是从`node_modules`文件夹加载的。 如果指定的路径不存在,`require()`会抛出一个`code`属性为`'MODULE_NOT_FOUND'`的错误。 ### 从`node_modules`文件夹中加载 如果`require()`中的模块名不是一个本地模块,也没有以`'/'`, `'../'`, 或是 `'./'`开头,那么node会从当前模块的父目录开始,尝试在它的`/node_modules`文件夹里加载相应模块。 如果没有找到,那么就再向上移动到父目录,直到到达顶层目录位置。 例如,如果位于`'/home/ry/projects/foo.js'`的文件调用了`require('bar.js')`,那么node查找的位置依次为: - `/home/ry/projects/node_modules/bar.js` - `/home/ry/node_modules/bar.js` - `/home/node_modules/bar.js` - `/node_modules/bar.js` 这就要求程序员应尽量把依赖放在就近的位置,以防崩溃。 ### Folders as Modules 可以把程序和库放到一个单独的文件夹里,并提供单一入口来指向它。有三种方法,使一个文件夹可以作为`require()`的参数来加载。 首先是在文件夹的根目录创建一个叫做`package.json`的文件,它需要指定一个`main`模块。下面是一个package.json文件的示例。 ~~~ { "name" : "some-library", "main" : "./lib/some-library.js" } ~~~ 示例中这个文件,如果是放在`./some-library`目录下面,那么`require('./some-library')`就将会去加载`./some-library/lib/some-library.js`。 This is the extent of Node's awareness of package.json files. 如果目录里没有package.json这个文件,那么node就会尝试去加载这个路径下的`index.js`或者`index.node`。例如,若上面例子中,没有package.json,那么`require('./some-library')`就将尝试加载下面的文件: - `./some-library/index.js` - `./some-library/index.node` ### Caching 模块在第一次加载后会被缓存。这意味着(类似其他缓存)每次调用`require('foo')`的时候都会返回同一个对象,当然,必须是每次都解析到同一个文件。 Multiple calls to `require('foo')` may not cause the module code to be executed multiple times. This is an important feature. With it, "partially done" objects can be returned, thus allowing transitive dependencies to be loaded even when they would cause cycles. 如果你希望一个模块多次执行,那么就输出一个函数,然后调用这个函数。 ### Module Caching Caveats 模块的缓存是依赖于解析后的文件名。由于随着调用的位置不同,可能解析到不同的文件(比如需从`node_modules`文件夹加载的情况),所以,如果解析到其他文件时,就不能*保证*`require('foo')`总是会返回确切的同一对象。 ### The `module` Object - {Object} 在每一个模块中,变量 `module` 是一个代表当前模块的对象的引用。 特别地,`module.exports` 可以通过全局模块对象 `exports` 获取到。 `module` 不是事实上的全局对象,而更像是每个模块内部的。 ### module.exports - {Object} `module.exports` 对象是通过模块系统产生的。有时候这是难以接受的,许多人想让他们的模块是某个类的实例。 因此,将要导出的对象赋值给 `module.exports` 。例如,假设我们有一个模块称之为 `a.js` ~~~ // Do some work, and after some time emit // the 'ready' event from the module itself. setTimeout(function() { module.exports.emit('ready'); }, 1000); ~~~ 那么,在另一个文件中我们可以这样写 ~~~ var a = require('./a'); a.on('ready', function() { console.log('module a is ready'); }); ~~~ Note that assignment to `module.exports` must be done immediately. It cannot be done in any callbacks. This does not work: x.js: ~~~ setTimeout(function() { module.exports = { a: "hello" }; }, 0); ~~~ y.js: ~~~ var x = require('./x'); console.log(x.a); ~~~ ### module.require(id) - `id` {String} - Return: {Object} 已解析模块的 `module.exports` `module.require` 方法提供了一种像 `require()` 一样从最初的模块加载一个模块的方法。 注意,为了这样做,你必须取得一个对 `module` 对象的引用。 `require()` 返回 `module.exports`,并且 `module` 是一个典型的只能在特定模块作用域内有效的变量,如果要使用它,就必须明确的导出。 ### module.id - {String} 用于区别模块的标识符。通常是完全解析后的文件名。 ### module.filename - {String} 模块完全解析后的文件名。 ### module.loaded - {Boolean} 不论该模块是否加载完毕,或者正在加载的过程中。 ### module.parent - {Module Object} 引入这个模块的模块。 ### module.children - {Array} 这个模块引入的所有模块对象。 ### 总体来说... 为了获取调用 `require` 加载的确切的文件名,使用 `require.resolve()` 函数。 综上所述,下面用伪代码的高级算法形式表达了 require.resolve 是如何工作的: ~~~ NODE_MODULES_PATHS(START) 1. let PARTS = path split(START) 2. let ROOT = index of first instance of "node_modules" in PARTS, or 0 3. let I = count of PARTS - 1 4. let DIRS = [] 5. while I > ROOT, a. if PARTS[I] = "node_modules" CONTINUE c. DIR = path join(PARTS[0 .. I] + "node_modules") b. DIRS = DIRS + DIR c. let I = I - 1 6. return DIRS ~~~ ### 从全局文件夹加载 如果 `NODE_PATH` 环境变量设置为一个以冒号分割的绝对路径的列表, 找不到模块时 node 将会从这些路径中搜索模块。 (注意:在 windows 操作系统上,`NODE_PATH` 是以分号间隔的) 此外,node 将会搜索以下地址: - 1: `$HOME/.node_modules` - 2: `$HOME/.node_libraries` - 3: `$PREFIX/lib/node` `$HOME` 是用户的主目录,`$PREFIX` 是 node 里配置的 `node_prefix` 。 这些大多是由于历史原因。强烈建议读者将所依赖的模块放到 `node_modules` 文件夹里。 这样加载的更快也更可靠。 ### 访问主模块 当 Node 直接运行一个文件时,`require.main` 就被设置为它的 `module` 。 也就是说你可以判断一个文件是否是直接被运行的 ~~~ require.main === module ~~~ 对于一个 `foo.js` 文件,如果通过 `node foo.js` 运行是 `true` ,但是通过 `require('./foo')` 运行却是 `false` 。 因为 `module` 提供了一个 `filename` 属性(通常等于 `__filename`), 所以当前程序的入口点可以通过 `require.main.filename` 来获取。 ### 附录: 包管理技巧 Node 的 `require()` 函数的语义被设计的足够通用化,以支持各种常规目录结构。 包管理程序如 dpkg,rpm 和 npm 将不用修改就能够从 Node 模块构建本地包。 接下来我们将给你一个可行的目录结构建议: 假设我们希望将一个包的指定版本放在 `/usr/lib/node/<some-package>/<some-version>` 目录中。 包可以依赖于其他包。为了安装包 foo,可能需要安装包 bar 的一个指定版本。 包 bar 也可能有依赖关系,在某些情况下依赖关系可能发生冲突或者形成循环。 因为 Node 会查找它所加载的模块的真实路径(也就是说会解析符号链接), 然后按照上文描述的方式在 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('queue')`,将会获得符号链接 `/usr/lib/node/bar/4.3.2/node_modules/quux` 指向的版本。 此外,为了进一步优化模块搜索过程,不要将包直接放在 `/usr/lib/node` 目录中,而是将它们放在 `/usr/lib/node_modules/<name>/<version>` 目录中。 这样在依赖的包找不到的情况下,就不会一直寻找 `/usr/node_modules` 目录或 `/node_modules` 目录了。 为了使模块在 node 的 REPL 中可用,你可能需要将 `/usr/lib/node_modules` 目录加入到 `$NODE_PATH` 环境变量中。 由于在 node_modules 目录中搜索模块使用的是相对路径,基于调用 `require()` 的文件所在真实路径,因此包本身可以放在任何位置。