以前在網上查閱了許多介紹Node.js的文章,惋惜對於Node.js的模塊機制大都着墨很少。在後續介紹模塊的使用以前,我認爲有必要深刻一下Node.js的模塊機制。 html
早在Netscape誕生不久後,JavaScript就一直在探索本地編程的路,Rhino是其表明產物。無奈那時服務端JavaScript走的路均是參考衆多服務器端語言來實現的,在這樣的背景之下,一沒有特點,二沒有實用價值。可是隨着JavaScript在前端的應用愈來愈普遍,以及服務端JavaScript的推進,JavaScript現有的規範十分薄弱,不利於JavaScript大規模的應用。那些以JavaScript爲宿主語言的環境中,只有自己的基礎原生對象和類型,更多的對象和API都取決於宿主的提供,因此,咱們能夠看到JavaScript缺乏這些功能: 前端
因而便有了CommonJS(http://www.commonjs.org)規範的出現,其目標是爲了構建JavaScript在包括Web服務器,桌面,命令行工具,及瀏覽器方面的生態系統。 node
CommonJS制定瞭解決這些問題的一些規範,而Node.js就是這些規範的一種實現。Node.js自身實現了require方法做爲其引入模塊的方法,同時NPM也基於CommonJS定義的包規範,實現了依賴管理和模塊自動安裝等功能。這裏咱們將深刻一下Node.js的require機制和NPM基於包規範的應用。 git
在Node.js中,定義一個模塊十分方便。咱們以計算圓形的面積和周長兩個方法爲例,來表現Node.js中模塊的定義方式。 github
var PI = Math.PI; exports.area = function (r) { return PI * r * r; }; exports.circumference = function (r) { return 2 * PI * r; };
將這個文件存爲circle.js,並新建一個app.js文件,並寫入如下代碼: web
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的模塊機制是很是優秀的。 express
Node.js的模塊分爲兩類,一類爲原生(核心)模塊,一類爲文件模塊。原生模塊在Node.js源代碼編譯的時候編譯進了二進制執行文件,加載的速度最快。另外一類文件模塊是動態加載的,加載速度比原生模塊慢。可是Node.js對原生模塊和文件模塊都進行了緩存,因而在第二次require時,是不會有重複開銷的。其中原生模塊都被定義在lib這個目錄下面,文件模塊則不定性。 npm
node app.js
因爲經過命令行加載啓動的文件幾乎都爲文件模塊。咱們從Node.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後綴的編譯過程。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,文件名,目錄名做爲實參並執行。
這就是爲何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中。
因爲Node.js中存在4類模塊(原生模塊和3種文件模塊),儘管require方法極其簡單,可是內部的加載倒是十分複雜的,其加載優先級也各自不一樣。
儘管原生模塊與文件模塊的優先級不一樣,可是都不會優先於從文件模塊的緩存中加載已經存在的模塊。
原生模塊的優先級僅次於文件模塊緩存的優先級。require方法在解析文件名以後,優先檢查模塊是否在原生模塊列表中。以http模塊爲例,儘管在目錄下存在一個http/http.js/http.node/http.json文件,require(「http」)都不會從這些文件中加載,而是從原生模塊中加載。
原生模塊也有一個緩存區,一樣也是優先從緩存區加載。若是緩存區沒有被加載過,則調用原生模塊的加載方式進行加載和執行。
當文件模塊緩存中不存在,並且不是原生模塊的時候,Node.js會解析require方法傳入的參數,並從文件系統中加載實際的文件,加載過程當中的包裝和編譯細節在前一節中已經介紹過,這裏咱們將詳細描述查找文件模塊的過程,其中,也有一些細節值得知曉。
require方法接受如下幾種參數的傳遞:
在進入路徑查找以前有必要描述一下module path這個Node.js中的概念。對於每個被加載的文件模塊,建立這個模塊對象的時候,這個模塊便會有一個paths屬性,其值根據當前文件的路徑計算獲得。咱們建立modulepath.js這樣一個文件,其內容爲:
console.log(module.paths);
咱們將其放到任意一個目錄中執行node modulepath.js命令,將獲得如下的輸出結果。
[ '/home/jackson/research/node_modules', '/home/jackson/node_modules', '/home/node_modules', '/node_modules' ]
Windows下:
[ 'c:\\nodejs\\node_modules', 'c:\\node_modules' ]
能夠看出module path的生成規則爲:從當前文件目錄開始查找node_modules目錄;而後依次進入父目錄,查找父目錄下的node_modules目錄;依次迭代,直到根目錄下的node_modules目錄。
除此以外還有一個全局module path,是當前node執行文件的相對目錄(../../lib/node)。若是在環境變量中設置了HOME目錄和NODE_PATH目錄的話,整個路徑還包含NODE_PATH和HOME目錄下的.node_libraries與.node_modules。其最終值大體以下:
[NODE_PATH,HOME/.node_modules,HOME/.node_libraries,execPath/../../lib/node]
下圖是筆者從源代碼中整理出來的整個文件查找流程:
簡而言之,若是require絕對路徑的文件,查找時不會去遍歷每個node_modules目錄,其速度最快。其他流程以下:
整個查找過程十分相似原型鏈的查找和做用域的查找。所幸Node.js對路徑查找實現了緩存機制,不然因爲每次判斷路徑都是同步阻塞式進行,會致使嚴重的性能消耗。
前面提到,JavaScript缺乏包結構。CommonJS致力於改變這種現狀,因而定義了包的結構規範(http://wiki.commonjs.org/wiki/Packages/1.0 )。而NPM的出現則是爲了在CommonJS規範的基礎上,實現解決包的安裝卸載,依賴管理,版本管理等問題。require的查找機制明瞭以後,咱們來看一下包的細節。
一個符合CommonJS規範的包應該是以下這種結構:
由上文的require的查找過程能夠知道,Node.js在沒有找到目標文件時,會將當前目錄看成一個包來嘗試加載,因此在package.json文件中最重要的一個字段就是main。而實際上,這一處是Node.js的擴展,標準定義中並不包含此字段,對於require,只須要main屬性便可。可是在除此以外包須要接受安裝、卸載、依賴管理,版本管理等流程,因此CommonJS爲package.json文件定義了以下一些必須的字段:
"contributors": [{ "name": "Jackson Tian", "email": "mail @gmail.com" }, { "name": "fengmk2", "email": "mail2@gmail.com" }],
"licenses": [{ "type": "GPLv2", "url": "http://www.example.com/licenses/gpl.html", }]
如下是Express框架的package.json文件,值得參考。
{ "name": "express", "description": "Sinatra inspired web development framework", "version": "3.0.0alpha1-pre", "author": "TJ Holowaychuk
除了前面提到的幾個必選字段外,咱們還發現了一些額外的字段,如bin、scripts、engines、devDependencies、author。這裏能夠重點說起一下scripts字段。包管理器(NPM)在對包進行安裝或者卸載的時候須要進行一些編譯或者清除的工做,scripts字段的對象指明瞭在進行操做時運行哪一個文件,或者執行拿條命令。以下爲一個較全面的scripts案例:
"scripts": { "install": "install.js", "uninstall": "uninstall.js", "build": "build.js", "doc": "make-doc.js", "test": "test.js", }
若是你完善了本身的JavaScript庫,使之實現了CommonJS的包規範,那麼你能夠經過NPM來發布本身的包,爲NPM上5000+的基礎上再加一個模塊。
npm publish <folder>
命令十分簡單。可是在這以前你須要經過npm adduser命令在NPM上註冊一個賬戶,以便後續包的維護。NPM會分析該文件夾下的package.json文件,而後上傳目錄到NPM的站點上。用戶在使用你的包時,也十分簡明:
npm install <package>
甚至對於NPM沒法安裝的包(由於某些奇怪的網絡緣由),能夠經過github手動下載其穩定版本,解壓以後經過如下命令進行安裝:
npm install <package.json folder>
只需將路徑指向package.json存在的目錄便可。而後在代碼中require('package')便可使用。
Node.js中的require內部流程之複雜,而方法調用之簡單,實在值得歎爲觀止。更多NPM使用技巧能夠參見http://www.infoq.com/cn/articles/msh-using-npm-manage-node.js-dependence。
一般有一些模塊能夠同時適用於先後端,可是在瀏覽器端經過script標籤的載入JavaScript文件的方式與Node.js不一樣。Node.js在載入到最終的執行中,進行了包裝,使得每一個文件中的變量自然的造成在一個閉包之中,不會污染全局變量。而瀏覽器端則一般是裸露的JavaScript代碼片斷。因此爲了解決先後端一致性的問題,類庫開發者須要將類庫代碼包裝在一個閉包內。如下代碼片斷抽取自著名類庫underscore的定義方式。
(function () { // Establish the root object, `window` in the browser, or `global` on the server. var root = this; var _ = function (obj) { return new wrapper(obj); }; if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = _; } exports._ = _; } else if (typeof define === 'function' && define.amd) { // Register as a named module with AMD. define('underscore', function () { return _; }); } else { root['_'] = _; } }).call(this);
首先,它經過function定義構建了一個閉包,將this做爲上下文對象直接call調用,以免內部變量污染到全局做用域。續而經過判斷exports是否存在來決定將局部變量_綁定給exports,而且根據define變量是否存在,做爲處理在實現了AMD規範環境(http://wiki.commonjs.org/wiki/Modules/AsynchronousDefinition)下的使用案例。僅只當處於瀏覽器的環境中的時候,this指向的是全局對象(window對象),纔將_變量賦在全局對象上,做爲一個全局對象的方法導出,以供外部調用。
因此在設計先後端通用的JavaScript類庫時,都有着如下相似的判斷:
if (typeof exports !== "undefined") { exports.EventProxy = EventProxy; } else { this.EventProxy = EventProxy; }
即,若是exports對象存在,則將局部變量掛載在exports對象上,若是不存在,則掛載在全局對象上。
對於更多前端的模塊實現能夠參考國內淘寶玉伯的seajs(http://seajs.com/),或者思科杜歡的oye(http://www.w3cgroup.com/oye/)。