1. 程式人生 > >nodejs 模組載入機制

nodejs 模組載入機制

Nodejs擁有一套簡單的模組載入系統,在Nodejs裡面檔案和模組是一一對應的關係。例如:foo.js載入了同一個目錄下的circle.js檔案。

circle.js檔案內容:

const PI = Math.PI;  
exports.area = (r) => PI*r*r;  
exports.circumference = (r) => 2*PI*r;  

foo.js檔案內容:

const circle = require('./circle.js');  
console.log(`the area of radius 4 is ${circle.area(4
)}`);

circle模組匯出了areacircumference函式,為了根模組能夠引用到它,你可以把它們新增到exports物件上。

模組內部的區域性變數都是私有的, 因為每個模組都被封裝在一個函式內部。上面的例中PI就是屬於circle模組的區域性變數。

如果你希望匯出一個函式或者一個物件,你應該把該函式或者物件賦值給module.exports而不是exports

訪問主模組

如果一個模組直接通過Node.js啟動執行,require.main將會設定為該模組。你可以通過如下方式測試當前模組是否為主模組:

console.log(require
.main === module);

舉個例子,對於foo.js檔案,如果通過nodejs foo.js執行,那麼該測試將會輸出true, 如果通過require('foo.js'),測試將輸出false
由於每個module物件都有一個filename屬性,也可以通過require.main.filename檢視主模組檔名。

模組載入

當我們呼叫require載入外部檔案的時候,將會呼叫require.resolve函式。具體的解析規則如下:

在Y目錄下的模組呼叫require(X)  

- 1 . 如果X是一個內建核心模組,  
   a. 返回該模組  
   b. 停止執行  

- 2
. 如果X使用`./`或`/`或`../`開a. 把(Y+X)作為檔案路徑來載入(LOAD_AS_FILE) b. 把(Y+X)作為目錄路徑來載入(LOAD_AS_DIRECTORY) - 3. 載入`node_modules(X, dirname(Y))`(LOAD_NODE_MODULES) - 4. 丟擲`not found`異

LOAD_AS_FILE(x)

  • A. 如果x是一個檔案則把x作為javascript文字檔案載入。 停止
  • B. 如果x.js是一個檔案則把x.js作為javascript文字檔案載入。 停止
  • C. 如果x.json是一個檔案則把x.json作為一個javascript物件來解析。 停止
  • D. 如果x.node是一個檔案則把x.node作為一個二進位制外掛。停止

LOAD_AS_DIRECTORY(x)

  • A. 如果x/package.json是一個檔案
    a. 解析package.json讀取main欄位.
    b. let m = x + main欄位值
    c. LOAD_AS_FILE(m)
  • B. 如果x/index.js是一個檔案,則把x/index.js作為javascript文字檔案載入。 停止
  • C. 如果x/index.json是一個檔案,則把x/index.json作為js物件來解析。停止
  • D. 如果x/index.node是一個檔案, 則把x/index.node作為二進位制外掛載入。停止

NODE_MODULES_PATHS(START)

  • let PARTS = path split(START)
  • let I = count of PARTS - 1
  • let DIRS = []
  • 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
  • return DIRS

模組快取

模組在首次載入完畢之後會被快取, 這意味著require('foo.js')不會導致foo.js被執行兩次。 如果希望多次執行模組程式碼,可以匯出export一個函式,該函式負責執行程式碼。

模組快取警告

  • 1、模組基於被解析的名字來快取,由於同一個模組在不同目錄被載入可能會得到不同的檔名,所以require('foo'),不能保證總是得到相同的物件。

  • 2、在一些大小寫不敏感的系統,不同的檔名被系統指向同一個檔案,但是快取模組依舊認為它們是兩個不同的模組,也就是說,require('foo')require('FOO')將會得到兩個不同的物件,而不考慮,fooFOO是否是同一個檔案。

核心模組

Nodejs內建幾個被打成二進位制形式的包。內建模組將會被優先載入,例如require('http')將會載入內建的http模組,即使有一個檔名也為http

需要注意的是:核心模組在安裝的時候已經和node可執行程式打包到一起了。

迴圈載入

考慮有這樣幾個模組:
a.js

console.log('a starting');
exports.done = false;
const 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;
const 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');
const a = require('./a.js');
const 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模組將會返回給b.js,接著b.js模組載入完畢,把exports物件返回給a.js

於此同時main.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.js將會嘗試載入不同字尾名的檔案主要有:(.js, .json, .node)。
.js字尾被解析為js文字檔案,.json檔案被解析文js物件,.node檔案被解析為node外掛,通過dlopen載入。

資料夾作為模組

使用資料夾是一個非常便捷的程式碼管理方式,提供一個統一的外部入口,供外部呼叫該資料夾。主要有三種方式可以達到該目的:

  • 在資料夾的根目錄建立一個package.json,使用main欄位指定入口指令碼檔案,例如:
{ "name" : "some-library",
  "main" : "./lib/some-library.js" }

在當前目錄下有一個some-library資料夾,此時呼叫require('./some-library')將會嘗試載入./some-library/lib/some-library.js檔案。如果main欄位指定的檔案找不到,Node.js將會報錯:

Error: Cannot find module 'some-library'

如果在該資料夾下沒有package.json,Node.js將會嘗試載入:

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

node_modules資料夾載入

如果傳遞給require的引數既不是內建模組,模組名稱也不是以.//../開頭,那麼Node.js將會嘗試尋找父目錄下的node_modules資料夾。如果沒有找到就再往上面一層查詢,直到退回系統根目錄。

舉個例子:檔案/home/ry/project/foo.js,呼叫require('bar.js'),將會查詢以下node_modules資料夾:

  • /home/ry/project/node_modules/bar.js
  • /home/ry/node_modules/bar.js
  • /home/node_modules/bar.js
  • /node_modules/bar.js

從全域性資料夾載入模組

NODE_PATH環境變數被配置為用一系列冒號分隔的絕對路徑,Nodejs將會去這些目錄下尋找模組。NODE_PATH最初在前面的一些模組載入方法都沒有出現的時候使用,現在慢慢變得沒那麼必要了。
除此之外Node.js還會查詢以下目錄:

  • $HOME/.node_modules
  • $HOME/.node_libraries
  • $PREFIX/lib/node_modules

$HOME為當前使用者的根目錄,$PREFIX通過node_prefix來配置。

基於一些歷史方面的原因,建議把模組安裝在本地的node_modules資料夾下,這一載入速度最快也最可靠。

模組包裝

在模組執行之前,Node.js把它包裝成一個函式的形式,看起來像這樣:

(function (exports, require, module, __filename, __dirname) {
// Your module code actually lives in here
});  

通過這種做法帶來以下好處:

  • 保證被let,var,const定義的變數作用域侷限於模組內部,而不是全域性變數。
  • 包裝了幾個看起來類似於全域性變數來指定該模組,例如:
    • moduleexports變數,用於從該模組匯出資料到其他模組。
    • __filename,__dirname,指向該模組的檔案絕對路徑以及資料夾路徑。

module物件

  • module.children
    一個數組指定了當前模組引用的其他模組。

  • module.exports
    一個物件,用於匯出資料,如果希望匯出的是一個函式,則應該給module.exports賦值,而不是給exports。否則會造成意想不到的後果。

  • exports
    一個物件,最初指向module.exports,如果你給它賦值,它將會指向物件,而不是最初的module.exports
    類似於:

function require(...) {
  // ...
  ((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;
}
  • module.filename
    模組的絕對路徑名稱

  • module.id
    模組id通常等於module.filename

  • module.loaded
    用於判斷模組是否載入完畢,或者正在載入中。

  • module.parent
    指向首次載入本模組的模組。

  • module.require(id)

    • id: String
    • 返回一個module.exports匯出的物件。