io.js
又一個簡單的模塊加載系統(tǒng)。在io.js
中,文件和模塊是一一對應的。以下例子中,foo.js
加載的同目錄下的circle.js
。
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()
函數(shù)和circumference()
函數(shù)。想要為你的模塊添加函數(shù)或?qū)ο螅憧梢詫⑺鼈兲砑又撂厥獾?code>exports對象的屬性上。
模塊的本地變量是私有的,好似模塊被包裹在一個函數(shù)中。在這個例子中變量PI
是circle.js
私有的。
如果想要你的模塊暴露一個函數(shù)(例如一個構造函數(shù)),或者想要一次賦值就暴露一個完整的對象,而不是一次綁定一個屬性,那就將之賦值給module.exports
而不是exports
。
以下,bar.js
使用了暴露了一個構造函數(shù)的square
模塊:
var square = require('./square.js');
var mySquare = square(2);
console.log('The area of my square is ' + mySquare.area());
square
模塊內(nèi)部:
// 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)的require()
調(diào)用。一個模塊可能在返回時,被沒有被執(zhí)行完畢。
考慮一下情況:
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
。為了避免一個無限循環(huán),a.js
會返回一個未完成的副本給b.js
模塊。b.js
會接著完成加載,然后它所暴露的值再被提供給a.js
模塊。
這樣main.js
就完成了它們的加載。因此程序的輸出是:
$ iojs 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)依賴,請確保它們按你的計劃工作。
io.js
中有一些模塊是被編譯成二進制的。這些模塊會在本文檔的其他地方詳細討論。
核心模塊被定義在io.js
源碼的lib/
目錄下。
當被require()
時,核心模塊總是被優(yōu)先加載的。例如require('http')
總是會返回內(nèi)建的HTTP模塊,甚至是有一個同名文件時。
如果準確的文件名沒有被發(fā)現(xiàn),那么io.js
將會依次添加.js
,.json
或.node
后綴名,然后試圖去加載。
.js
文件被解釋為JavaScript
文本文件,.json
被解釋為JSON
文本文件,.node
文件被解釋為編譯好的插件模塊,然后被dlopen
加載。
前綴是'/'
則是文件的絕對路徑。例如require('/home/marco/foo.js')
將會加載/home/marco/foo.js
。
前綴是'./'
則是調(diào)用require()
的文件的相對路徑。也就是說,circle.js
必須與foo.js
在同一目錄下,這樣require('./circle')
才能找到它。
如果沒有'/'
,'./'
或'../'
前綴,模塊要么是一個核心模塊,或是需要從node_modules
目錄中被加載。
如果指定的路徑不存在,require()
將會拋出一個code
屬性是'MODULE_NOT_FOUND'
的錯誤。
如果傳遞給require()
的模塊標識符不是一個本地模塊,也沒有以'/'
,'../'
或'./'
開始。那么io.js
將會從當前目錄的父目錄開始,添加/node_modules
,試圖從這個路徑來加載模塊。
如果還是沒有找到模塊,那么它會再移至此目錄的父目錄,如此往復,直至到達文件系統(tǒng)的根目錄。
例如,如果一個位于'/home/ry/projects/foo.js'
的文件調(diào)用了require('bar.js')
,那么io.js
將會按照以下的路徑順序來查找:
/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
這要求程序本地化(localize)自己的依賴,防止它們崩潰。
你也可以在模塊名中加入一個路徑后綴,來引用這個模塊中特定的一個文件或子模塊。例如,require('example-module/path/to/file')
將會從example-module
的位置解析相對路徑path/to/file
。路徑后綴遵循相同的模塊解析語義。
在一個單獨目錄下組織程序和庫,然后提供一個單獨的入口,是非常便捷的。有三種方法,可以將目錄作為require()
的參數(shù),來加載模塊。
第一種方法是,在模塊的根目錄下創(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
。
這就是io.js
所能夠了解package.json
文件的程度。
如果目錄中沒有package.json
文件,那么io.js
將會視圖去加載當前目錄中的index.js
或index.node
。例如,如果在上面的例子中沒有package.json
,那么require('./some-library')
將會試圖加載:
./some-library/index.js
./some-library/index.node
模塊在第一次被加載后,會被緩存。這意味著,如果都解析到了相同的文件,每一次調(diào)用require('foo')
都將會返回同一個對象。
多次調(diào)用require('foo')
可能不會造成模塊代碼被執(zhí)行多次。這是一個重要的特性。有了它,“部分完成”的對象也可以被返回,這樣,傳遞依賴也能被加載,即使它們可能會造成循環(huán)依賴。
如果你想要一個模塊被多次執(zhí)行,那么就暴露一個函數(shù),然后執(zhí)行這個函數(shù)。
模塊的緩存依賴于它們被解析后的文件名。所以調(diào)用模塊的位置不同,可以會解析出不同的文件名(比如需要從node_modules目錄中加載)。所以不能保證require('foo')
總是會返回相同的對象,因為它們可能被解析為了不同的文件。
每一個模塊中,變量module
是一個代表了當前模塊的引用。為了方便,module.exports
也可以通過模塊作用域中的exports
取得。module
對象實際上不是全局的,而是每個模塊本地的。
module.exports
對象是由模塊系統(tǒng)創(chuàng)建的。有時這是難以接受的;許多人希望它們的模塊是一些類的實例。如果需要這樣,那么就將想要暴露的對象賦值給module.exports
。注意,將想要暴露的對象傳遞給exports
,將僅僅只會重新綁定(rebind)本地變量exports
,所以不要這么做。
例如假設我們正在寫一個叫做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
的賦值必須立刻完成。它不能在任何的回調(diào)函數(shù)中完成。以下例子將不能正常工作:
x.js
:
setTimeout(function() {
module.exports = { a: "hello" };
}, 0);
y.js
:
var x = require('./x');
console.log(x.a);
exports
變量是一個module.exports
的引用。如果你將一個新的值賦予它,那么它將不再指向先前的那個值。
為了說明這個行為,將require()
的實現(xiàn)假設為這樣:
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
之間的關系,請只使用module.exports
。
module.exports
module.require
方法提供了一種像require()
一樣,從源模塊中加載模塊的方法。
注意,為了這么做,你必須取得module
對象的引用。因為require()
返回module.exports
,并且module
對象是一個典型的只在特定的模塊作用域中有效的變量,如果要使用它,必須被明確地導出。
模塊的識別符。通常是被完全解析的文件名。
模塊完全解析后的文件名。
模塊是否加載完成,或者是正在加載的過程中。
引用這個模塊的模塊。
這個模塊所引入的模塊。
為了獲得require()
被調(diào)用時將要被加載的準確文件名,使用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
如果NODE_PATH
環(huán)境變量被設置為了一個以冒號分割的絕對路徑列表,那么在找不到模塊時,io.js
將會從這些路徑中尋找模塊(注意:在Windows中,NODE_PATH
是以分號間隔的)。
NODE_PATH
最初被創(chuàng)建,是用來支持在當前的模塊解析算法被凍結(frozen)前,從不同的路徑加載模塊的。
NODE_PATH
仍然被支持,但是,如今io.js
生態(tài)圈已經(jīng)有了放置依賴模塊的公約,它已經(jīng)不那么必要的。有時,當人們沒有意識到NODE_PATH
有被設置時,依賴于NODE_PATH
的部署可能會產(chǎn)生出人意料的表現(xiàn)。有時,一個模塊的依賴改變了,造成了通過NODE_PATH
,加載了不同版本的模塊。
另外,io.js
將會查找以下路徑:
$HOME
是用戶的家目錄,$PREFIX
是io.js
中配置的node_prefix
。
由于一些歷史原因,高度推薦你將依賴放入node_modules
目錄。它會被加載的更快,且可靠性更好。
當一個文件直接由io.js
執(zhí)行,require.main
將被設置為這個模塊。這意味著你可以判斷一個文件是否是直接被運行的。
require.main === module
對于一個文件foo.js
,如果通過iojs foo.js
運行,以上將會返回true
。如果通過require('./foo')
,將會返回false
。
因為module
提供了一個filename
屬性(通常等于__filename
),所以當前應用的入口點可以通過檢查require.main.filename
來獲取。
io.js
的require()
函數(shù)的語義被設計得足夠通用,來支持各種目錄結構。包管理程序諸如dpkg
,rpm
和npm
將可以通過不修改io.js
模塊,來構建本地包。
以下我們給出一個建議的可行的目錄結構:
假設/usr/lib/node/<some-package>/<some-version>
中有指定版本包的內(nèi)容。
包可以依賴于其他包。為了安裝foo
包,你可能需要安裝特定版本的bar
包。bar
包可能有它自己的依賴,在一些情況下,它們的依賴可以會沖突或者產(chǎn)生循環(huán)。
由于io.js
會查找任何它加載的包得真實路徑(也就是說,解析symlinks
),解析以下結構的方案非常簡單:
foo
包的內(nèi)容,1.2.3
版本。foo
包所依賴的bar
包的內(nèi)容。/usr/lib/node/bar/4.3.2/
的符號鏈接。bar
包所依賴的包的符號鏈接。因此,即使有循環(huán)依賴,或者依賴沖突,每個模塊都能夠獲取它們使用的特定版本的依賴。
當foo
包中的代碼執(zhí)行require('bar')
,將會獲得符號鏈接/usr/lib/node/foo/1.2.3/node_modules/bar
指向的版本。接著,bar
包種的代碼執(zhí)行require('quux')
,它將會獲得符號鏈接/usr/lib/node/bar/4.3.2/node_modules/quux
指向的版本。
此外,為了優(yōu)化模塊查找的過程,我們將模塊放在/usr/lib/node_modules/<name>/<version>
而不是直接放在/usr/lib/node
中。然后在找不到依賴時,io.js
就不會一直去查找/usr/node_modules
或/node_modules
目錄了。
為了讓模塊在io.js
的REPL中可用,可能需要將/usr/lib/node_modules
目錄加入到$NODE_PATH
環(huán)境變量。因為使用node_modules
目錄的模塊查找都是使用相對路徑,且基于調(diào)用require()
的文件的真實路徑,因此包本身可以在任何位置。
更多建議: