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
模組匯出了area
和circumference
函式,為了根模組能夠引用到它,你可以把它們新增到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')
將會得到兩個不同的物件,而不考慮,foo
和FOO
是否是同一個檔案。
核心模組
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
定義的變數作用域侷限於模組內部,而不是全域性變數。 - 包裝了幾個看起來類似於全域性變數來指定該模組,例如:
module
和exports
變數,用於從該模組匯出資料到其他模組。__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匯出的物件。