穩(wěn)定性: 5 - 鎖定
本節(jié)介紹Node.js的模塊系統(tǒng)。
Node.js有簡單的模塊加載系統(tǒng)。在Node.js模塊系統(tǒng)中,每個文件都可以被當(dāng)作單獨的模塊。下面例子里,foo.js
對同一個文件夾里的circle.js
模塊進(jìn)行加載。這是foo.js
內(nèi)容:
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is '
+ circle.area(4));
這是circle.js
內(nèi)容:
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
circle.js
模塊輸出了area()
和circumference()
函數(shù)。想要給根模塊添加函數(shù)和對象,你可以將他們添加到特定的exports
對象。
加載到模塊的變量是私有的,仿佛模塊是包含在一個函數(shù)里。在這個例子里,PI
是circle.js
的私有變量。
如果你想模塊里的根像一個函數(shù)一樣的輸出(比如,構(gòu)造函數(shù)),或者你想輸出一個完整對象,那就分派給module.exports
,而不是exports
。
bar.js
使用square
模塊,它輸出了構(gòu)造函數(shù):
var square = require('./square.js');
var mySquare = square(2);
console.log('The area of my square is ' + mySquare.area());
square
定義在square.js
文件里:
// assigning to exports will not modify module, must use module.exports
module.exports = function(width) {
return {
area: function() {
return width * width;
}
};
}
模塊系統(tǒng)在require("module")
模塊里實現(xiàn)。
環(huán)形調(diào)用require()
,當(dāng)返回時模塊可能都沒執(zhí)行結(jié)束。
考慮以下場景:
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);
當(dāng)main.js
加載a.js
,a.js
加載b.js
。此時,b.js
試著加載a.js
。為了阻止循環(huán)調(diào)用,a.js
輸出對象的不完全拷貝返回給b.js
模塊。b.js
會結(jié)束加載,并且它的exports
對象提供給a.js
模塊。
main.js
加載完兩個模塊時,它們都會結(jié)束。這個程序的輸出如下:
$ 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
如果你的程序有環(huán)形模塊依賴,需要保證是線性的。
Node有很多模塊編譯成二進(jìn)制。這些模塊在本文檔的其他地方有更詳細(xì)的描述。
核心模塊定義在Node的源代碼lib/
目錄里。
require()
總是會優(yōu)先加載核心模塊。例如,require('http')
總是返回編譯好的HTTP模塊,而不管這個文件的名字。
如果按照文件名沒有找到模塊,那么Node會試著加載添加了.js
和.json
后綴的文件,如果還沒好到,再試著加載添加了后綴.node
的文件。
.js
會解析為JavaScript的文本文件,.json
會解析為JSON文本文件,.node
會解析為編譯過的插件模塊,由dlopen
負(fù)責(zé)加載。
模塊的前綴'/'
表示絕對路徑。例如require('/home/marco/foo.js')
將會加載 /home/marco/foo.js
文件。
模塊的前綴'./'
表示相對于調(diào)用require()
的路徑。就是說,circle.js
必須和foo.js
在同一個目錄里,require('./circle')
才能找到。
文件前沒有/
或./
前綴,表示模塊可能是core module
,或者已經(jīng)從node_modules
文件夾里加載過了。
如果指定的路徑不存在,require()
將會拋出一個code
屬性為'MODULE_NOT_FOUND'
的異常。
node_modules
目錄里加載如傳遞給require()
的模塊不是一個本地模塊,并且不以'/'
,'../'
或'./'
開頭,那么Node會從當(dāng)前模塊的父目錄開始,嘗試在它的node_modules
文件夾里加載模塊。
如果沒有找到,那么會到父目錄,直到到文件系統(tǒng)的根目錄里找。
例如,如果'/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
這樣允許程序獨立,不會產(chǎn)生沖突。
可以請求指定的文件或分布子目錄里的模塊,在模塊名后添加路徑后綴。例如,require('example-module/path/to/file')
會解決path/to/file
相對于example-module
的加載位置。路徑后綴使用相同語法。
可以把程序和庫放到獨立的文件夾里,并提供單一的入口指向他們。有三種方法可以將文件夾作為參數(shù)傳給require()
。
第一個方法是,在文件夾的根創(chuàng)建一個package.json
文件,它指定了main
模塊。package.json
的例子如下:
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
如果這是在./some-library
里的文件夾,require('./some-library')
將會試著加載./some-library/lib/some-library.js
。
如果文件夾里沒有package.json
文件,Node會試著加載index.js
或index.node
文件。例如,如果上面的例子里沒有package.json文件。那么 require('./some-library')
將會試著加載:
./some-library/index.js
./some-library/index.node
模塊第一次加載后會被被緩存。這就是說,每次調(diào)用require('foo')
都會返回同一個對象,當(dāng)然,必須每次都要解析到同一個文件。
多次調(diào)用require('foo')
也許不會導(dǎo)致模塊代碼多次執(zhí)行。這是很重要的特性,這樣就可以返回"partially done"對象,允許加載過渡性的依賴關(guān)系,即使可能會引起環(huán)形調(diào)用。
如果你希望多次調(diào)用一個模塊,那么就輸出一個函數(shù),然后調(diào)用這個函數(shù)。
模塊的緩存依賴于解析后的文件名。因此隨著調(diào)用位置的不同,模塊可能解析到不同的文件(例如,從node_modules
文件夾加載)。如果解析為不同的文件,require('foo')
可能會返回不同的對象。
module
對象在每個模塊中,變量module
是一個代表當(dāng)前模塊的對象的引用。為了方便,module.exports
可以通過exports
全局模塊訪問。module
不是事實上的全局對象,而是每個模塊內(nèi)部的。
模塊系統(tǒng)創(chuàng)建module.exports
對象。很多人希望自己的模塊是某個類的實例。因此,把將要導(dǎo)出的對象賦值給module.exports
。注意,將想要的對象賦值給 exports
,只是簡單的將它綁定到本地exports
變量,這可能并不是你想要的。
例如,假設(shè)我們有一個模塊叫a.js
。
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
// 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');
});
注意:賦給module.exports
必須馬上執(zhí)行,并且不能在回調(diào)中執(zhí)行。
x.js:
setTimeout(function() {
module.exports = { a: "hello" };
}, 0);
y.js:
var x = require('./x');
console.log(x.a);
exports
變量在引用到module.exports
的模塊里可用。和其他變量一樣,如果你給他賦一個新的值,它不再指向老的值。
為了展示這個特性,假設(shè)實現(xiàn):require()
:
function require(...) {
// ...
function (module, exports) {
// Your module code here
exports = some_func; // re-assigns exports, exports is no longer
// a shortcut, and nothing is exported.
module.exports = some_func; // makes your module export 0
} (module, module.exports);
return module;
}
如果你對exports
和module.exports
間的關(guān)系感到迷糊,那就只用module.exports
就好。
id
{String}module.exports
module.require
方法提供了一種像require()
一樣從最初的模塊加載一個模塊的方法。
為了能這樣做,你必須獲得module
對象的引用。require()
返回module.exports
,并且module
是一個典型的只能在特定模塊作用域內(nèi)有效的變量,如果要使用它,就必須明確的導(dǎo)出。
模塊的標(biāo)識符。通常是完全解析的文件名。
模塊完全解析的文件名。
模塊是已經(jīng)加載完畢,還是在加載中。
引入這個模塊的模塊。
由這個模塊引入的模塊。
為了獲取即將用require()
加載的準(zhǔn)確文件名,可以使用require.resolve()
函數(shù)。
綜上所述,下面用偽代碼的高級算法形式演示了require.resolve的工作流程:
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"
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_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)
2. If X/index.js is a file, load X/index.js as JavaScript text. STOP
3. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
4. If X/index.node is a file, load X/index.node as binary addon. STOP
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 = []
4. while I >= 0,
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
5. return DIRS
如果環(huán)境變量NODE_PATH
設(shè)置為冒號分割的絕對路徑列表,并且在模塊在其他地方?jīng)]有找到,Node將會搜索這些路徑。(注意,在Windows系統(tǒng)中,NODE_PATH
用分號分割 )。
另外,Node將會搜索這些路徑。
$HOME/.node_modules
$HOME/.node_libraries
$PREFIX/lib/node
$HOME
是用戶的home文件夾,$PREFIX
是Node里配置的node_prefix
。
這大多是歷史原因照成的。強(qiáng)烈建議將所以來的模塊放到node_modules
文件夾里。這樣加載會更快。
當(dāng)Node運(yùn)行一個文件時,require.main
就會設(shè)置為它的module
。也就是說你可以通過測試判斷文件是否被直接運(yùn)行。
require.main === module
對于foo.js
文件。 如果直接運(yùn)行node foo.js
,返回true
,如果通過require('./foo')
是間接運(yùn)行。
因為module
提供了filename
屬性(通常等于__filename
),程序的入口點可以通過檢查require.main.filename
來獲得。
Node的require()
函數(shù)語義定義的足夠通用,它能支持各種常規(guī)目錄結(jié)構(gòu)。諸如dpkg
,rpm
和npm
包管理程序,不用修改就可以從Node模塊構(gòu)建本地包。
下面我們介紹一個可行的目錄結(jié)構(gòu):
假設(shè)我們有一個/usr/lib/node/<some-package>/<some-version>
文件夾,它包含指定版本的包內(nèi)容。
一個包可以依賴于其他包。為了安裝包foo,可能需要安裝特定版本的bar
包。bar
包可能有自己的包依賴,某些條件下,依賴關(guān)系可能會發(fā)生沖突或形成循環(huán)。
因為Node會查找他所加載的模塊的realpath
(也就是說會解析符號鏈接),然后按照上文描述的方式在node_modules目錄中尋找依賴關(guān)系,這種情形跟以下體系結(jié)構(gòu)非常相像:
/usr/lib/node/foo/1.2.3/
- foo
包,version 1.2.3。/usr/lib/node/bar/4.3.2/
- foo
依賴的bar
包內(nèi)容。/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
包所依賴的包的符號鏈接。因此,即使存在循環(huán)依賴或依賴沖突,每個模塊還可以獲得他所依賴的包得可用版本。
當(dāng)foo
包里的代碼調(diào)用foo
,將會獲得符號鏈接/usr/lib/node/foo/1.2.3/node_modules/bar
指向的版本。然后,當(dāng)bar包中的代碼調(diào)用 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
目錄了。基于調(diào)用require()的文件所在真實路徑,因此包本身可以放在任何位置。
為了讓Node模塊對Node REPL可用,可能需要將/usr/lib/node_modules
文件夾路徑添加到環(huán)境變量$NODE_PATH
。由于模塊查找$NODE_PATH
文件夾都是相對路徑,因此包可以放到任何位置。
更多建議: