nodejs 模組機制
簡單模組定義和使用
在Node.js中,定義一個模組十分方便。我們以計算圓形的面積和周長兩個方法為例,來表現Node.js中模組的定義方式。
var PI = Math.PI;
exports.area = function (r) {
return PI * r * r;
};
exports.circumference = function (r) {
return 2 * PI * r;
};
複製程式碼
將這個檔案存為circle.js,然後新建一個app.js檔案,並寫入以下程式碼:
var circle = require('./circle.js');
console.log( 'The area of a circle of radius 4 is ' + circle.area(4));
複製程式碼
可以看到模組呼叫也十分方便,只需要require需要呼叫的檔案即可。
在require了這個檔案之後,定義在exports物件上的方法便可以隨意呼叫。Node.js將模組的定義和呼叫都封裝得極其簡單方便,從API對使用者友好這一個角度來說,Node.js的模組機制是非常優秀的。
關於exports的內容,可以參考之前的文章 exports && module.exports
模組分類
核心模組
核心模組優先順序僅次於快取載入,因此無法載入一個和核心模組識別符號相同的自定義模組。
路徑形式的檔案模組
以"."、".."開頭和"/"開始的識別符號,這裡都被當作檔案模組來處理。require()方法會將路徑轉為真實路徑,並以真實路徑作為索引,並將編譯執行後的結果存放到快取中。
自定義模組(特殊的檔案模組)
自定義模組是指非核心模組,也不是路徑形式的識別符號。它是一種特殊的檔案模組,可能是一個檔案或者包的形式。 模組路徑是Node在定位檔案模組的具體檔案時制定的查詢策略,具體表現為一個路徑組成的陣列(module.paths)。這個路徑由當前目錄開始往上一直到根目錄,Node會逐個嘗試模組路徑中的路徑,直到找到目標檔案未知,若到達根目錄還是沒有找到目標檔案,則會丟擲查詢失敗的異常。當前檔案的目錄越深,模組查詢耗時越多。
模組載入策略
上文中說道,Node.js的模組分為兩類,一類為原生(核心)模組,一類為檔案模組。
原生模組在Node.js原始碼編譯的時候編譯進了二進位制執行檔案,載入的速度最快。另一類檔案模組是動態載入的,載入速度比原生模組慢。但是Node.js對原生模組和檔案模組都進行了快取,於是在第二次require時,是不會有重複開銷的。由於通過命令列載入啟動的檔案幾乎都為檔案模組。我們從Node.js如何載入檔案模組開始談起。
我們從命令列啟動上文的app.js
檔案。
node app.js
複製程式碼
載入檔案模組的工作,主要由原生模組module
來實現和完成,該原生模組在啟動時已經被載入,程式直接呼叫到runMain
靜態方法。
// bootstrap main module.
Module.runMain = function () {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
};
複製程式碼
_load
靜態方法在分析檔名之後執行
var module = new Module(id, parent);
複製程式碼
並根據檔案路徑快取當前模組物件,該模組例項物件則根據檔名載入。
module.load(filename);
複製程式碼
實際上在檔案模組中,又分為3類模組。這三類檔案模組以字尾來區分,Node.js會根據字尾名來決定載入方法。
- .js。通過fs模組同步讀取js檔案並編譯執行。
- .node。通過C/C++進行編寫的Addon。通過dlopen方法進行載入。
- .json。讀取檔案,呼叫JSON.parse解析載入。
這裡我們將詳細描述js字尾的編譯過程。Node.js在編譯js檔案的過程中實際完成的步驟有對js檔案內容進行頭尾包裝。以app.js
為例,包裝之後的app.js
將會變成以下形式:
(function (exports, require, module, __filename, __dirname) {
var circle = require('./circle.js');
console.log('The area of a circle of radius 4 is ' + circle.area(4));
});
複製程式碼
這段程式碼會通過vm
原生模組的runInThisContext
方法執行(類似eval
,只是具有明確上下文,不汙染全域性),返回為一個具體的function
物件。最後傳入module
物件的exports
,require
方法,module
,__filename
(檔名),__dirname
(目錄名)作為實參並執行。
這就是為什麼require
並沒有定義在app.js
檔案中,但是這個方法卻存在的原因。從Node.js的API文件中可以看到還有__filename
、__dirname
、module
、exports
幾個沒有定義但是卻存在的變數。
__filename``和__dirname
在查詢檔案路徑的過程中分析得到後傳入的。module
變數是這個模組物件自身,exports
是在module的建構函式中初始化的一個空物件({},而不是null)。
在這個主檔案中,可以通過require
方法去引入其餘的模組。而其實這個require
方法實際呼叫的就是load
方法。
load
方法在載入、編譯、快取了module
後,返回module
的exports
物件。這就是circle.js
檔案中只有定義在exports
物件上的方法才能被外部呼叫的原因。
以上所描述的模組載入機制均定義在lib/module.js
中。
require 方法中的檔案查詢策略
儘管require方法極其簡單,但是內部的載入卻是十分複雜的,其載入優先順序也各自不同。
從檔案載入
當檔案模組快取中不存在,而且不是原生模組的時候,Node.js會解析require
方法傳入的引數,並從檔案系統中載入實際的檔案,載入過程中的包裝和編譯細節在前一節中已經介紹過,這裡我們將詳細描述查詢檔案模組的過程,其中,也有一些細節值得知曉。
require方法接受以下幾種引數的傳遞:
- http、fs、path等,原生模組。
- ./mod或../mod,相對路徑的檔案模組。
- /pathtomodule/mod,絕對路徑的檔案模組。
- mod,非原生模組的檔案模組。
在進入路徑查詢之前有必要描述一下module path
這個Node.js中的概念。對於每一個被載入的檔案模組,建立這個模組物件的時候,這個模組便會有一個paths
屬性,其值根據當前檔案的路徑計算得到。我們建立modulepath.js
這樣一個檔案,其內容為:
console.log(module.paths);
複製程式碼
我們將其放到任意一個目錄中執行node modulepath.js命令,將得到以下的輸出結果(mac的演示結果)。
[ '/Users/beifeng/Desktop/test_node/node_modules',
'/Users/beifeng/Desktop/node_modules',
'/Users/beifeng/node_modules',
'/Users/node_modules',
'/node_modules' ]
複製程式碼
可以看出module path的生成規則為:從當前檔案目錄開始查詢
node_modules
目錄;然後依次進入父目錄,查詢父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules
目錄。
檔案模組查詢流程
簡而言之,如果require
絕對路徑的檔案,查詢時不會去遍歷每一個node_modules
目錄,其速度最快。其餘流程如下:
1.從module paths
陣列中取出第一個目錄作為查詢基準。
2.直接從目錄中查詢該檔案,如果存在,則結束查詢。如果不存在,則進行下一條查詢
3.嘗試新增.js
、.json
、.node
字尾後查詢,如果存在檔案,則結束查詢。如果不存在,則進行下一條。
4.嘗試將require
的引數作為一個包來進行查詢,讀取目錄下的package.json
檔案,取得main
引數指定的檔案。
5.嘗試查詢該檔案,如果存在,則結束查詢。如果不存在,則進行第3條查詢。
6.如果繼續失敗,則取出module path
陣列中的下一個目錄作為基準查詢,迴圈第1至5個步驟。
7.如果繼續失敗,迴圈第1至6個步驟,直到module paths
中的最後一個值。
8.如果仍然失敗,則丟擲異常。
整個查詢過程十分類似原型鏈的查詢和作用域的查詢。所幸Node.js對路徑查詢實現了快取機制,否則由於每次判斷路徑都是同步阻塞式進行,會導致嚴重的效能消耗。
CommonJS規範
JavaScript缺少包結構。CommonJS致力於改變這種現狀,於是定義了包的結構規範(http://wiki.commonjs.org/wiki/Packages/1.0 )。
CommonJS(http://www.commonjs.org)規範的出現,其目標是為了構建JavaScript在包括Web伺服器,桌面,命令列工具,及瀏覽器方面的生態系統。
一個符合CommonJS規範的包應該是如下這種結構:
- 一個package.json檔案應該存在於包頂級目錄下
- 二進位制檔案應該包含在bin目錄下。
- JavaScript程式碼應該包含在lib目錄下。
- 文件應該在doc目錄下。
- 單元測試應該在test目錄下。
由上文的require的查詢過程可以知道,Node.js在沒有找到目標檔案時,會將當前目錄當作一個包來嘗試載入,所以在package.json檔案中最重要的一個欄位就是main。而實際上,這一處是Node.js的擴充套件,標準定義中並不包含此欄位,對於require,只需要main屬性即可。
引用文章
-
深入淺出Nodejs