# 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()` 的文件所在真實路徑,因此包本身可以放在任何位置。