IO.js Modules

2018-11-28 22:34 更新

穩(wěn)定度: 3 - 鎖定

io.js又一個(gè)簡(jiǎn)單的模塊加載系統(tǒng)。在io.js中,文件和模塊是一一對(duì)應(yīng)的。以下例子中,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對(duì)象的屬性上。

模塊的本地變量是私有的,好似模塊被包裹在一個(gè)函數(shù)中。在這個(gè)例子中變量PIcircle.js私有的。

如果想要你的模塊暴露一個(gè)函數(shù)(例如一個(gè)構(gòu)造函數(shù)),或者想要一次賦值就暴露一個(gè)完整的對(duì)象,而不是一次綁定一個(gè)屬性,那就將之賦值給module.exports而不是exports。

以下,bar.js使用了暴露了一個(gè)構(gòu)造函數(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")中被實(shí)現(xiàn)。

循環(huán)依賴

當(dāng)存在循環(huán)的require()調(diào)用。一個(gè)模塊可能在返回時(shí),被沒(méi)有被執(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);

當(dāng)main.js加載a.js,而后a.js會(huì)去加載b.js。與此同時(shí),b.js嘗試去加載a.js。為了避免一個(gè)無(wú)限循環(huán),a.js會(huì)返回一個(gè)未完成的副本給b.js模塊。b.js會(huì)接著完成加載,然后它所暴露的值再被提供給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)依賴,請(qǐng)確保它們按你的計(jì)劃工作。

核心模塊

io.js中有一些模塊是被編譯成二進(jìn)制的。這些模塊會(huì)在本文檔的其他地方詳細(xì)討論。

核心模塊被定義在io.js源碼的lib/目錄下。

當(dāng)被require()時(shí),核心模塊總是被優(yōu)先加載的。例如require('http')總是會(huì)返回內(nèi)建的HTTP模塊,甚至是有一個(gè)同名文件時(shí)。

文件模塊

如果準(zhǔn)確的文件名沒(méi)有被發(fā)現(xiàn),那么io.js將會(huì)依次添加.js,.json.node后綴名,然后試圖去加載。

.js文件被解釋為JavaScript文本文件,.json被解釋為JSON文本文件,.node文件被解釋為編譯好的插件模塊,然后被dlopen加載。

前綴是'/'則是文件的絕對(duì)路徑。例如require('/home/marco/foo.js')將會(huì)加載/home/marco/foo.js。

前綴是'./'則是調(diào)用require()的文件的相對(duì)路徑。也就是說(shuō),circle.js必須與foo.js在同一目錄下,這樣require('./circle')才能找到它。

如果沒(méi)有'/','./''../'前綴,模塊要么是一個(gè)核心模塊,或是需要從node_modules目錄中被加載。

如果指定的路徑不存在,require()將會(huì)拋出一個(gè)code屬性是'MODULE_NOT_FOUND'的錯(cuò)誤。

從node_modules目錄中加載

如果傳遞給require()的模塊標(biāo)識(shí)符不是一個(gè)本地模塊,也沒(méi)有以'/''../''./'開(kāi)始。那么io.js將會(huì)從當(dāng)前目錄的父目錄開(kāi)始,添加/node_modules,試圖從這個(gè)路徑來(lái)加載模塊。

如果還是沒(méi)有找到模塊,那么它會(huì)再移至此目錄的父目錄,如此往復(fù),直至到達(dá)文件系統(tǒng)的根目錄。

例如,如果一個(gè)位于'/home/ry/projects/foo.js'的文件調(diào)用了require('bar.js'),那么io.js將會(huì)按照以下的路徑順序來(lái)查找:

/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js

這要求程序本地化(localize)自己的依賴,防止它們崩潰。

你也可以在模塊名中加入一個(gè)路徑后綴,來(lái)引用這個(gè)模塊中特定的一個(gè)文件或子模塊。例如,require('example-module/path/to/file')將會(huì)從example-module的位置解析相對(duì)路徑path/to/file。路徑后綴遵循相同的模塊解析語(yǔ)義。

作為模塊的目錄

在一個(gè)單獨(dú)目錄下組織程序和庫(kù),然后提供一個(gè)單獨(dú)的入口,是非常便捷的。有三種方法,可以將目錄作為require()的參數(shù),來(lái)加載模塊。

第一種方法是,在模塊的根目錄下創(chuàng)建一個(gè)package.json文件,其中指定了main模塊。一個(gè)示例package.json文件:

{ "name" : "some-library",
  "main" : "./lib/some-library.js" }

如果這個(gè)文件位于./some-library,那么require('./some-library')將會(huì)試圖去加載./some-library/lib/some-library.js。

這就是io.js所能夠了解package.json文件的程度。

如果目錄中沒(méi)有package.json文件,那么io.js將會(huì)視圖去加載當(dāng)前目錄中的index.jsindex.node。例如,如果在上面的例子中沒(méi)有package.json,那么require('./some-library')將會(huì)試圖加載:

./some-library/index.js
./some-library/index.node

緩存

模塊在第一次被加載后,會(huì)被緩存。這意味著,如果都解析到了相同的文件,每一次調(diào)用require('foo')都將會(huì)返回同一個(gè)對(duì)象。

多次調(diào)用require('foo')可能不會(huì)造成模塊代碼被執(zhí)行多次。這是一個(gè)重要的特性。有了它,“部分完成”的對(duì)象也可以被返回,這樣,傳遞依賴也能被加載,即使它們可能會(huì)造成循環(huán)依賴。

如果你想要一個(gè)模塊被多次執(zhí)行,那么就暴露一個(gè)函數(shù),然后執(zhí)行這個(gè)函數(shù)。

模塊緩存警告

模塊的緩存依賴于它們被解析后的文件名。所以調(diào)用模塊的位置不同,可以會(huì)解析出不同的文件名(比如需要從node_modules目錄中加載)。所以不能保證require('foo')總是會(huì)返回相同的對(duì)象,因?yàn)樗鼈兛赡鼙唤馕鰹榱瞬煌奈募?/p>

module對(duì)象

  • {Object}

每一個(gè)模塊中,變量module是一個(gè)代表了當(dāng)前模塊的引用。為了方便,module.exports也可以通過(guò)模塊作用域中的exports取得。module對(duì)象實(shí)際上不是全局的,而是每個(gè)模塊本地的。

module.exports

  • Object

module.exports對(duì)象是由模塊系統(tǒng)創(chuàng)建的。有時(shí)這是難以接受的;許多人希望它們的模塊是一些類的實(shí)例。如果需要這樣,那么就將想要暴露的對(duì)象賦值給module.exports。注意,將想要暴露的對(duì)象傳遞給exports,將僅僅只會(huì)重新綁定(rebind)本地變量exports,所以不要這么做。

例如假設(shè)我們正在寫一個(gè)叫做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);

那么在另一個(gè)文件中我們可以:

var a = require('./a');
a.on('ready', function() {
  console.log('module a is ready');
});

主要,對(duì)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快捷方式

exports變量是一個(gè)module.exports的引用。如果你將一個(gè)新的值賦予它,那么它將不再指向先前的那個(gè)值。

為了說(shuō)明這個(gè)行為,將require()的實(shí)現(xiàn)假設(shè)為這樣:

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;
}

一個(gè)指導(dǎo)方針是,如果你弄不清楚exportsmodule.exports之間的關(guān)系,請(qǐng)只使用module.exports

module.require(id)

  • id String
  • Return: 被解析的模塊的module.exports

module.require方法提供了一種像require()一樣,從源模塊中加載模塊的方法。

注意,為了這么做,你必須取得module對(duì)象的引用。因?yàn)?code>require()返回module.exports,并且module對(duì)象是一個(gè)典型的只在特定的模塊作用域中有效的變量,如果要使用它,必須被明確地導(dǎo)出。

module.id

  • String

模塊的識(shí)別符。通常是被完全解析的文件名。

module.filename

  • String

模塊完全解析后的文件名。

module.loaded

  • Boolean

模塊是否加載完成,或者是正在加載的過(guò)程中。

module.parent

  • Module Object

引用這個(gè)模塊的模塊。

module.children

  • Array

這個(gè)模塊所引入的模塊。

總體來(lái)說(shuō)

為了獲得require()被調(diào)用時(shí)將要被加載的準(zhǔn)確文件名,使用require.resolve()函數(shù)。

綜上所述,以下是一個(gè)require.resolve所做的事的高級(jí)算法偽代碼:

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)境變量被設(shè)置為了一個(gè)以冒號(hào)分割的絕對(duì)路徑列表,那么在找不到模塊時(shí),io.js將會(huì)從這些路徑中尋找模塊(注意:在Windows中,NODE_PATH是以分號(hào)間隔的)。

NODE_PATH最初被創(chuàng)建,是用來(lái)支持在當(dāng)前的模塊解析算法被凍結(jié)(frozen)前,從不同的路徑加載模塊的。

NODE_PATH仍然被支持,但是,如今io.js生態(tài)圈已經(jīng)有了放置依賴模塊的公約,它已經(jīng)不那么必要的。有時(shí),當(dāng)人們沒(méi)有意識(shí)到NODE_PATH有被設(shè)置時(shí),依賴于NODE_PATH的部署可能會(huì)產(chǎn)生出人意料的表現(xiàn)。有時(shí),一個(gè)模塊的依賴改變了,造成了通過(guò)NODE_PATH,加載了不同版本的模塊。

另外,io.js將會(huì)查找以下路徑:

  • 1: $HOME/.node_modules
  • 2: $HOME/.node_libraries
  • 3: $PREFIX/lib/node

$HOME是用戶的家目錄,$PREFIXio.js中配置的node_prefix。

由于一些歷史原因,高度推薦你將依賴放入node_modules目錄。它會(huì)被加載的更快,且可靠性更好。

訪問(wèn)主模塊

當(dāng)一個(gè)文件直接由io.js執(zhí)行,require.main將被設(shè)置為這個(gè)模塊。這意味著你可以判斷一個(gè)文件是否是直接被運(yùn)行的。

require.main === module

對(duì)于一個(gè)文件foo.js,如果通過(guò)iojs foo.js運(yùn)行,以上將會(huì)返回true。如果通過(guò)require('./foo'),將會(huì)返回false。

因?yàn)?code>module提供了一個(gè)filename屬性(通常等于__filename),所以當(dāng)前應(yīng)用的入口點(diǎn)可以通過(guò)檢查require.main.filename來(lái)獲取。

附錄:包管理小貼士

io.jsrequire()函數(shù)的語(yǔ)義被設(shè)計(jì)得足夠通用,來(lái)支持各種目錄結(jié)構(gòu)。包管理程序諸如dpkg,rpmnpm將可以通過(guò)不修改io.js模塊,來(lái)構(gòu)建本地包。

以下我們給出一個(gè)建議的可行的目錄結(jié)構(gòu):

假設(shè)/usr/lib/node/<some-package>/<some-version>中有指定版本包的內(nèi)容。

包可以依賴于其他包。為了安裝foo包,你可能需要安裝特定版本的bar包。bar包可能有它自己的依賴,在一些情況下,它們的依賴可以會(huì)沖突或者產(chǎn)生循環(huán)。

由于io.js會(huì)查找任何它加載的包得真實(shí)路徑(也就是說(shuō),解析symlinks),解析以下結(jié)構(gòu)的方案非常簡(jiǎn)單:

  • /usr/lib/node/foo/1.2.3/ - foo包的內(nèi)容,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/的符號(hào)鏈接。
  • /usr/lib/node/bar/4.3.2/node_modules/* - 指向bar包所依賴的包的符號(hào)鏈接。

因此,即使有循環(huán)依賴,或者依賴沖突,每個(gè)模塊都能夠獲取它們使用的特定版本的依賴。

當(dāng)foo包中的代碼執(zhí)行require('bar'),將會(huì)獲得符號(hào)鏈接/usr/lib/node/foo/1.2.3/node_modules/bar指向的版本。接著,bar包種的代碼執(zhí)行require('quux'),它將會(huì)獲得符號(hào)鏈接/usr/lib/node/bar/4.3.2/node_modules/quux指向的版本。

此外,為了優(yōu)化模塊查找的過(guò)程,我們將模塊放在/usr/lib/node_modules/<name>/<version>而不是直接放在/usr/lib/node中。然后在找不到依賴時(shí),io.js就不會(huì)一直去查找/usr/node_modules/node_modules目錄了。

為了讓模塊在io.js的REPL中可用,可能需要將/usr/lib/node_modules目錄加入到$NODE_PATH環(huán)境變量。因?yàn)槭褂?code>node_modules目錄的模塊查找都是使用相對(duì)路徑,且基于調(diào)用require()的文件的真實(shí)路徑,因此包本身可以在任何位置。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)