Node.js入門:模組機制
**CommonJS規範 **
早在Netscape誕生不久後,JavaScript就一直在探索本地程式設計的路,Rhino是其代表產物。無奈那時服務端JavaScript走的路均是參考眾多伺服器端語言來實現的,在這樣的背景之下,一沒有特色,二沒有實用價值。但是隨著JavaScript在前端的應用越來越廣泛,以及服務端JavaScript的推動,JavaScript現有的規範十分薄弱,不利於JavaScript大規模的應用。那些以JavaScript為宿主語言的環境中,只有本身的基礎原生物件和型別,更多的物件和API都取決於宿主的提供,所以,我們可以看到JavaScript缺少這些功能:
- JavaScript沒有模組系統。沒有原生的支援密閉作用域或依賴管理。
- JavaScript沒有標準庫。除了一些核心庫外,沒有檔案系統的API,沒有IO流API等。
- JavaScript沒有標準介面。沒有如Web Server或者資料庫的統一介面。
- JavaScript沒有包管理系統。不能自動載入和安裝依賴。
於是便有了CommonJS(http://www.commonjs.org)規範的出現,其目標是為了構建JavaScript在包括Web伺服器,桌面,命令列工具,及瀏覽器方面的生態系統。CommonJS制定瞭解決這些問題的一些規範,而Node.js就是這些規範的一種實現。Node.js自身實現了require方法作為其引入模組的方法,同時NPM也基於CommonJS定義的包規範,實現了依賴管理和模組自動安裝等功能。這裡我們將深入一下Node.js的require機制和NPM基於包規範的應用。
簡單模組定義和使用
在Node.js中,定義一個模組十分方便。我們以計算圓形的面積和周長兩個方法為例,來表現Node.js中模組的定義方式。
1 var PI = Math.PI; 2 exports.area = function (r) { 3 return PI * r * r; 4 }; 5 exports.circumference = function (r) { 6 return 2 * PI * r; 7 };</pre> }//歡迎加入全棧開發交流圈一起學習交流:582735936 ]//面向1-3年前端人員 } //幫助突破技術瓶頸,提升思維能力
將這個檔案存為circle.js,並新建一個app.js檔案,並寫入以下程式碼:
1 var circle = require('./circle.js');
2 console.log( 'The area of a circle of radius
3 is ' + circle.area(
4));</pre>
可以看到模組呼叫也十分方便,只需要require需要呼叫的檔案即可。
在require了這個檔案之後,定義在exports物件上的方法便可以隨意呼叫。Node.js將模組的定義和呼叫都封裝得極其簡單方便,從API對使用者友好這一個角度來說,Node.js的模組機制是非常優秀的。
模組載入策略
Node.js的模組分為兩類,一類為原生(核心)模組,一類為檔案模組。原生模組在Node.js原始碼編譯的時候編譯進了二進位制執行檔案,載入的速度最快。另一類檔案模組是動態載入的,載入速度比原生模組慢。但是Node.js對原生模組和檔案模組都進行了快取,於是在第二次require時,是不會有重複開銷的。其中原生模組都被定義在lib這個目錄下面,檔案模組則不定性。
node app.js
由於通過命令列載入啟動的檔案幾乎都為檔案模組。我們從Node.js如何載入檔案模組開始談起。載入檔案模組的工作,主要由原生模組module來實現和完成,該原生模組在啟動時已經被載入,程序直接呼叫到runMain靜態方法。
1 // bootstrap main module.
2 Module.runMain = function () {
3 // Load the main module--the command line argument.
4 Module._load(process.argv[1], null, true); 5 };</pre>
_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將會變成以下形式:
1 (function (exports, require, module, __filename, __dirname) {
2 var circle = require('./circle.js');
3 console.log('The area of a circle of radius
4 is ' + circle.area(4)); 4 });</pre>
這段程式碼會通過vm原生模組的runInThisContext方法執行(類似eval,只是具有明確上下文,不汙染全域性),返回為一個具體的function物件。最後傳入module物件的exports,require方法,module,檔名,目錄名作為實參並執行。
這就是為什麼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中。