1. 程式人生 > >Node.js入門:模組機制

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中。