Node.js入門:模塊機制

CommonJS規範

早在Netscape誕生不久後,JavaScript就一直在探索本地編程的路,Rhino是其代表產物。無奈那時服務端JavaScript走的路均是參考眾多服務器端語言來實現的,在這樣的背景之下,一沒有特色,二沒有實用價值。但是隨著JavaScript在前端的應用越來越廣泛,以及服務端JavaScript的推動,JavaScript現有的規範十分薄弱,不利於JavaScript大規模的應用。那些以JavaScript為宿主語言的環境中,只有本身的基礎原生對象和類型,更多的對象和API都取決於宿主的提供,所以,我們可以看到JavaScript缺少這些功能:

  • JavaScript沒有模塊系統。沒有原生的支持密閉作用域或依賴管理。
  • JavaScript沒有標準庫。除了一些核心庫外,沒有文件系統的API,沒有IO流API等。
  • JavaScript沒有標準接口。沒有如Web Server或者數據庫的統一接口。
  • JavaScript沒有包管理系統。不能自動加載和安裝依賴。

於是便有了CommonJS(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 };

}//歡迎加入全棧開發交流圈一起學習交流: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));

複製代碼

可以看到模塊調用也十分方便,只需要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 };

複製代碼

_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 });

複製代碼

這段代碼會通過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中。


分享到:


相關文章: