1. 程式人生 > >【深入淺出Node.js系列三】深入Node.js的模組機制

【深入淺出Node.js系列三】深入Node.js的模組機制

1 Node.js模組的實現

之前在網上查閱了許多介紹Node.js的文章,可惜對於Node.js的模組機制大都著墨不多。在後續介紹模組的使用之前,我認為有必要深入一下Node.js的模組機制

1.1 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其實不是一門新的語言,甚至都不能說它是一個新的直譯器——實際上它只是一個概念或者是一個規範

在這個規範中,它定義了很多 API ,講通俗點或者直截了當點就是函式啊類啊什麼的,而這些 API 是為那些普通應用程式(Native App)而非瀏覽器應用使用

。它的終極目標就是提供一個類似於 Python、Ruby 之類的指令碼一樣的標準庫,開發者可以用這樣的東西一樣來做到 Python、Ruby 能做到的事,而非僅僅侷限於網頁中的效果或者功能實現,它也可以跑在本地。所以說下面的事情對於 JavaScript 來說不再是夢:

服務端JavaScript應用

命令列工具

圖形介面應用

混合應用(Titanium、Adobe AIR等)

那麼,它具體彌補了 前端JavaScript 的哪些空白呢?其實這也涉及了很多 前端JavaScript 所沒有涉及的東西,如二進位制、編碼、IO、檔案、系統、斷言測試、套接字、事件佇列、Worker、控制檯等等

CommonJS制定瞭解決這些問題的一些規範,而Node.js就是這些規範的一種實現Node.js自身實現了require方法作為其引入模組的方法,同時NPM也基於CommonJS定義的包規範,實現了依賴管理和模組自動安裝等功能。這裡我們將深入一下Node.js的require機制和NPM基於包規範的應用。

1.2 簡單模組定義和使用

在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的模組機制是非常優秀的。

例如把我們的伺服器指令碼放到一個叫做 start 的函式裡,然後我們會匯出這個函式。程式碼放在server.js檔案:

var http = require("http");

functionstart(){
    functiononRequest(request, response){
        console.log("Request received.");
        response.writeHead(200, {"Content-Type": "text/plain"});
        response.write("Hello World");
        response.end();
    }

    http.createServer(onRequest).listen(8888);
    console.log("Server has started.");
}

exports.start = start;

這樣,我們現在就可以建立我們的主檔案 index.js 並在其中啟動我們的HTTP了,雖然伺服器的程式碼還在 server.js 中。建立 index.js 檔案並寫入以下內容:

var server = require("./server");

server.start();

1.3 exports與module.exports的區別

上一節已經使用了用來建立函式的exports物件,來匯出一個模組(假設一個名為rocker.js的檔案):

exports.name = function() {
    console.log('My name is Lemmy Kilmister');
};

然後你在另一個檔案中呼叫:

var rocker = require('./rocker.js');
rocker.name(); // 'My name is Lemmy Kilmister'

但是module.exports到底是個什麼玩意兒? 它合法嗎?

令人吃驚的是-module.exports是真實存在的東西。exports只是module.exports的輔助方法。你的模組最終返回module.exports給呼叫者,而不是exports。exports所做的事情是收集屬性,如果module.exports當前沒有任何屬性的話,exports會把這些屬性賦予module.exports。如果module.exports已經存在一些屬性的話,那麼exports中所用的東西都會被忽略

把下面的內容放到rocker.js:

module.exports = 'ROCK IT!';
exports.name = function(){
    console.log('My name is Lemmy Kilmister');
};

然後把下面的內容放到另一個檔案中,執行它:

var rocker = require('./rocker.js');
rocker.name(); // TypeError: Object ROCK IT! has no method 'name'

rocker模組完全忽略了exports.name,然後返回了一個字串'ROCK IT!'。通過上面的例子,你可能認識到你的模組不一定非得是模組例項(module instances)。你的模組可以是任何合法的JavaScript物件 - boolean,number,date,JSON, string,function,array和其他。你的模組可以是任何你賦予module.exports的值。如果你沒有明確的給module.exports設定任何值,那麼exports中的屬性會被賦給module.exports中,然後並返回它

在下面的情況下,你的模組是一個類:

module.exports = function(name, age){
    this.name = name;
    this.age = age;
    this.about = function(){
        console.log(this.name +' is '+ this.age +' years old');
    };
};

然後你應該這樣使用它:

var Rocker = require('./rocker.js');
var r = new Rocker('Ozzy', 62);
r.about(); // Ozzy is 62 years old

在下面的情況下,你的模組是一個數組:

module.exports = ['Lemmy Kilmister', 'Ozzy Osbourne', 'Ronnie James Dio', 'Steven Tyler', 'Mick Jagger'];

然後你應該這樣使用它:

var rocker = require('./rocker.js');
console.log('Rockin in heaven: ' + rocker[2]); //Rockin in heaven: Ronnie James Dio

現在你應該找到要點了 - 如果你想要你的模組成為一個特別的物件型別,那麼使用module.exports;如果你希望你的模組成為一個傳統的模組例項(module instance),使用exports

把屬性賦予module.exports的結果與把屬性賦予給exports是一樣的。看下面這個例子:

module.exports.name = function() {
    console.log('My name is Lemmy Kilmister');
};

下面這個做的是一樣的事情:

exports.name = function() {
    console.log('My name is Lemmy Kilmister');
};

但是請注意,它們並不是一樣的東西。就像我之前說的module.exports是真實存在的東西,exports只是它的輔助方法。話雖如此,exports還是推薦的物件,除非你想把你模組的物件型別從傳統的模組例項(module instance)修改為其他的

1.4 模組載入策略

Node.js的模組分為兩類,一類為原生(核心)模組,一類為檔案模組原生模組在Node.js原始碼編譯的時候編譯進了二進位制執行檔案,載入的速度最快。另一類檔案模組是動態載入的,載入速度比原生模組慢。但是Node.js對原生模組和檔案模組都進行了快取,於是在第二次require時,是不會有重複開銷的。其中原生模組都被定義在lib這個目錄下面,檔案模組則不定性

node app.js

由於通過命令列載入啟動的檔案幾乎都為檔案模組。我們從Node.js如何載入檔案模組開始談起。載入檔案模組的工作,主要由原生模組module來實現和完成,該原生模組在啟動時已經被載入,程序直接呼叫到runMain靜態方法

// bootstrap main module.
Module.runMain = function(){
    // Load the main module--the command line arg
    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,檔名,目錄名作為實參並執行

這就是為什麼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中。

1.5 require方法中的檔案查詢策略

由於Node.js中存在4類模組(原生模組和3種檔案模組),儘管require方法極其簡單,但是內部的載入卻是十分複雜的,其載入優先順序也各自不同。

輸入圖片說明

  1. 從檔案模組快取中載入

    儘管原生模組與檔案模組的優先順序不同,但是都不會優先於從檔案模組的快取 中載入已經存在的模組。

  2. 從原生模組載入

    原生模組的優先順序僅次於檔案模組快取的優先順序。require方法在解析檔名之後,優先檢查模組是否在原生模組列表中。以http模組為例,儘管在目錄下存在一個http/http.js/http.node/http.json檔案,require(“http”)都不會從這些檔案中載入,而是從原生模組中載入。

    原生模組也有一個快取區,同樣也是優先從快取區載入。如果快取區沒有被載入過,則呼叫原生模組的載入方式進行載入和執行。

  3. 從檔案載入

    當檔案模組快取中不存在,而且不是原生模組的時候,Node.js會解析require方法傳入的引數,並從檔案系統中載入實際的檔案,載入過程中的包裝和編譯細節在前一節中已經介紹過,這裡我們將詳細描述查詢檔案模組的過程,其中, 也有一些細節值得知曉。

require方法接受以下幾種引數的傳遞:

http、fs、path等,原生模組。

/mod或../mod,相對路徑的檔案模組。

/pathtomodule/mod,絕對路徑的檔案模組。

mod,非原生模組的檔案模組。

在進入路徑查詢之前有必要描述一下modulepath這個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'
]

可以看出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目錄,其速度最快。其餘流程如下:

  1. 從modulepath陣列中取出第一個目錄作為查詢基準。

  2. 直接從目錄中查詢該檔案,如果存在,則結束查詢。如果不存在,則進行下一條查詢。

  3. 嘗試新增.js、.json、.node字尾後查詢,如果存在檔案,則結束查詢。如果不存在,則進行下一條。

  4. 嘗試將require的引數作為一個包來進行查詢,讀取目錄下的package.json檔案,取得main引數指定的檔案。

  5. 嘗試查詢該檔案,如果存在,則結束查詢。如果不存在,則進行第3條查詢。

  6. 如果繼續失敗,則取出modulepath陣列中的下一個目錄作為基準查詢,迴圈第1至5個步驟。

  7. 如果繼續失敗,迴圈第1至6個步驟,直到modulepath中的最後一個值。

  8. 如果仍然失敗,則丟擲異常。

整個查詢過程十分類似原型鏈的查詢和作用域的查詢。所幸Node.js對路徑查詢實現了快取機制,否則由於每次判斷路徑都是同步阻塞式進行,會導致嚴重的效能消耗。

1.6 包結構

前面提到,JavaScript缺少包結構。CommonJS致力於改變這種現狀,於是定義了包的結構規範(http://wiki.commonjs.org/wiki/Packages/1.0)。而NPM的出現則是為了在CommonJS規範的基礎上,實現解決包的安裝解除安裝,依賴管理,版本管理等問題。require的查詢機制明瞭之後,我們來看一下包的細節。

一個符合CommonJS規範的包應該是如下這種結構:

一個package.json檔案應該存在於包頂級目錄下。

二進位制檔案應該包含在bin目錄下。

JavaScript程式碼應該包含在lib目錄下。

文件應該在doc目錄下。

單元測試應該在test目錄下。

由上文的require的查詢過程可以知道,Node.js在沒有找到目標檔案時,會將當前目錄當作一個包來嘗試載入,所以在package.json檔案中最重要的一個欄位就是main。而實際上,這一處是Node.js的擴充套件,標準定義中並不包含此欄位, 對於require,只需要main屬性即可。但是在除此之外包需要接受安裝、解除安裝、依賴管理,版本管理等流程,所以CommonJS為package.json檔案定義瞭如下一些必須的欄位:

name : 包名,需要在NPM上是唯一的。不能帶有空格。

description : 包簡介。通常會顯示在一些列表中。

version : 版本號。一個語義化的版本號(http://semver.org/),通常為x.y.z。該版本號十分重要,常常用於一些版本控制的場合。

keywords : 關鍵字陣列。用於NPM中的分類搜尋。

maintainers : 包維護者的陣列。陣列元素是一個包含name、email、web三個屬性的JSON物件。

contributors : 包貢獻者的陣列。第一個就是包的作者本人。在開源社群,如果提交的patch被merge進master分支的話,就應當加上這個貢獻patch的人。格式包含name和email。

"contributors": [{
         "name": "Jackson Tian", "email": "mail @gmail.com"
     }, {
         "name": "fengmk2", "email": "mail2@gmail.com"
 }],

bugs : 一個可以提交bug的URL地址。可以是郵件地址 (mailto:[email protected]),也可以是網頁地址(http://url)。

licenses : 包所使用的許可證。例如:

"licenses": [{
     "type": "GPLv2",
     "url": "http://www.example.com/licenses/gpl.html",
 }]

repositories : 託管原始碼的地址陣列。

dependencies : 當前包需要的依賴。這個屬性十分重要,NPM會通過這個屬性,幫你自動載入依賴的包。

以下是Express框架的package.json檔案,值得參考:

{
  "_args": [
    [
      "[email protected]~4.13.1",
      "/Users/TaoBangren/[email protected]/king-node"
    ]
  ],
  "_from": "[email protected]>=4.13.1 <4.14.0",
  "_id": "[email protected]",
  "_inCache": true,
  "_installable": true,
  "_location": "/express",
  "_npmUser": {
    "email": "[email protected]",
    "name": "dougwilson"},
  "_npmVersion": "1.4.28",
  "_phantomChildren": {},
  "_requested": {
    "name": "express",
    "raw": "[email protected]~4.13.1",
    "rawSpec": "~4.13.1",
    "scope": null,
    "spec": ">=4.13.1 <4.14.0",
    "type": "range"},
  "_requiredBy": [
    "/"
  ],
  "_resolved": "https://registry.npmjs.org/express/-/express-4.13.3.tgz",
  "_shasum": "ddb2f1fb4502bf33598d2b032b037960ca6c80a3",
  "_shrinkwrap": null,
  "_spec": "[email protected]~4.13.1",
  "_where": "/Users/TaoBangren/[email protected]/king-node",
  "author": {
    "email": "[email protected]",
    "name": "TJ Holowaychuk"},
  "bugs": {
    "url": "https://github.com/strongloop/express/issues"},
  "contributors": [
    {
      "name": "Aaron Heckmann",
      "email": "[email protected]"},
    {
      "name": "Ciaran Jessup",
      "email": "[email protected]"},
    {
      "name": "Douglas Christopher Wilson",
      "email": "[email protected]"},
    {
      "name": "Guillermo Rauch",
      "email": "[email protected]"},
    {
      "name": "Jonathan Ong",
      "email": "[email protected]"},
    {
      "name": "Roman Shtylman",
      "email": "[email protected]"},
    {
      "name": "Young Jae Sim",
      "email": "[email protected]"}
  ],
  "dependencies": {
    "accepts": "~1.2.12",
    "array-flatten": "1.1.1",
    "content-disposition": "0.5.0",
    "content-type": "~1.0.1",
    "cookie": "0.1.3",
    "cookie-signature": "1.0.6",
    "debug": "~2.2.0",
    "depd": "~1.0.1",
    "escape-html": "1.0.2",
    "etag": "~1.7.0",
    "finalhandler": "0.4.0",
    "fresh": "0.3.0",
    "merge-descriptors": "1.0.0",
    "methods": "~1.1.1",
    "on-finished": "~2.3.0",
    "parseurl": "~1.3.0",
    "path-to-regexp": "0.1.7",
    "proxy-addr": "~1.0.8",
    "qs": "4.0.0",
    "range-parser": "~1.0.2",
    "send": "0.13.0",
    "serve-static": "~1.10.0",
    "type-is": "~1.6.6",
    "utils-merge": "1.0.0",
    "vary": "~1.0.1"},
  "description": "Fast, unopinionated, minimalist web framework",
  "devDependencies": {
    "after": "0.8.1",
    "body-parser": "~1.13.3",
    "connect-redis": "~2.4.1",
    "cookie-parser": "~1.3.5",
    "cookie-session": "~1.2.0",
    "ejs": "2.3.3",
    "express-session": "~1.11.3",
    "istanbul": "0.3.17",
    "jade": "~1.11.0",
    "marked": "0.3.5",
    "method-override": "~2.3.5",
    "mocha": "2.2.5",
    "morgan": "~1.6.1",
    "multiparty": "~4.1.2",
    "should": "7.0.2",
    "supertest": "1.0.1",
    "vhost": "~3.0.1"},
  "directories": {},
  "dist": {
    "shasum": "ddb2f1fb4502bf33598d2b032b037960ca6c80a3",
    "tarball": "http://registry.npmjs.org/express/-/express-4.13.3.tgz"},
  "engines": {
    "node": ">= 0.10.0"},
  "files": [
    "History.md"
            
           

相關推薦

no