標籤:
本文為讀書筆記。
一、CommonJS的模組規範
Node與瀏覽器以及 W3C組織、CommonJS組織、ECMAScript之間的關係
Node借鑒CommonJS的Modules規範實現了一套模組系統,所以先來看看CommonJS的模組規範。
CommonJS對模組的定義十分簡單,主要分為模組引用、模組定義和模組標識3個部分。
1. 模組引用
模組引用的範例程式碼如下:
var math = require(‘math‘);
在CommonJS規範中,存在require()方法,這個方法接受模組標識,以此引入一個模組的API到當前上下文中。
2. 模組定義
在模組中,上下文提供require()方法來引入外部模組。對應引入的功能,上下文提供了exports對象用於匯出當前模組的方法或者變數,並且它是唯一匯出的出口。在模組中,還存在一個module對象,它代表模組自身,而exports是module的屬性。在Node中,一個檔案就是一個模組,將方法掛載在exports對象上作為屬性即可定義匯出的方式:
// math.js
exports.add = function () {
var sum = 0, i = 0, args = arguments, l = args.length;
while (i < l) { sum += args[i++]; }
return sum;
};
在另一個檔案中,我們通過require()方法引入模組後,就能調用定義的屬性或方法了:
// program.js
var math = require(‘math‘);
exports.increment = function (val) { return math.add(val, 1);};
3.模組標識
模組標識其實就是傳遞給require()方法的參數,它必須是符合小駝峰命名的字串,或者以.、..開頭的相對路徑,或者絕對路徑。它可以沒有檔案名稱尾碼.js。模組的定義十分簡單,介面也十分簡潔。它的意義在於將類聚的方法和變數等限定在私人的範圍中,同時支援引入和匯出功能以順暢地串連上下遊依賴。每個模組具有獨立的空間,它們互不干擾,在引用時也顯得乾淨利落。
二、Node的模組實現Node在實現中並非完全按照規範實現,而是對模組規範進行了一定的取捨,同時也增加了少許自身需要的特性。儘管規範中exports、require和module聽起來十分簡單,但是Node在實現它們的過程中究竟經曆了什麼,這個過程需要知曉。
在Node中引入模組,需要經曆如下3個步驟。
1. 路徑分析
2. 檔案定位
3. 編譯執行
在Node中,模組分為兩類:一類是Node提供的模組,稱為核心模組;另一類是使用者編寫的模組,稱為檔案模組。
? 核心模組部分在Node原始碼的編譯過程中,編譯進了二進位執行檔案。在Node進程啟動時,部分核心模組就被直接載入進記憶體中,所以這部分核心模組引入時,檔案定位和編譯執行這兩個步驟可以省略掉,並且在路徑分析中優先判斷,所以它的載入速度是最快的。
? 檔案模組則是在運行時動態載入,需要完整的路徑分析、檔案定位、編譯執行過程,速度比核心模組慢。
1.優先從緩衝載入
與前端瀏覽器會緩衝靜態指令檔以提高效能一樣,Node對引入過的模組都會進行緩衝,以減少二次引入時的開銷。不同的地方在於,瀏覽器僅僅快取檔案,而Node緩衝的是編譯和執行之後的對象。不論是核心模組還是檔案模組,require()方法對相同模組的二次載入都一律採用緩衝優先的方式,這是第一優先順序的。不同之處在於核心模組的緩衝檢查先於檔案模組的緩衝檢查。
2.路徑分析和檔案定位
因為標識符有幾種形式,對於不同的標識符,模組的尋找和定位有不同程度上的差異。
1. 模組標識符分析
Node基於一個模組標識符進行模組尋找。模組標識符在Node中主要分為以下幾類。
- 核心模組,如http、fs、path等。
- .或..開始的相對路徑檔案模組。
- 以/開始的絕對路徑檔案模組。
- 非路徑形式的檔案模組,如自訂的connect模組。
? 核心模組
核心模組的優先順序僅次於緩衝載入,它在Node的原始碼編譯過程中已經編譯為二進位代碼,其載入過程最快。如果試圖載入一個與核心模組標識符相同的自訂模組,那是不會成功的。如果自己編寫了一個http使用者模組,想要載入成功,必須選擇一個不同的標識符或者換用路徑的方式。
? 路徑形式的檔案模組
以.、..和/開始的標識符,這裡都被當做檔案模組來處理。在分析路徑模組時,require()方法會將路徑轉為真實路徑,並以真實路徑作為索引,將編譯執行後的結果存放到緩衝中,以使二次載入時更快。由於檔案模組給Node指明了確切的檔案位置,所以在尋找過程中可以節約大量時間,其載入速度慢於核心模組。
? 自訂模組
自訂模組指的是非核心模組,也不是路徑形式的標識符。它是一種特殊的檔案模組,可能是一個檔案或者包的形式。這類別模組的尋找是最費時的,也是所有方式中最慢的一種。
2.檔案定位
從緩衝載入的最佳化策略使得二次引入時不需要路徑分析、檔案定位和編譯執行的過程,大大提高了再次載入模組時的效率。但在檔案的定位過程中,還有一些細節需要注意,這主要包括副檔名的分析、目錄和包的處理。
? 副檔名分析
CommonJS模組規範也允許在標識符中不包含副檔名,這種情況下,Node會按.js、.json、.node的次序補足副檔名,依次嘗試。在嘗試的過程中,需要調用fs模組同步阻塞式地判斷檔案是否存在。因為Node是單線程的,所以這裡是一個會引起效能問題的地方。小訣竅是:如果是.node和.json檔案,在傳遞給require()的標識符中帶上副檔名,會加快一點速度。
? 目錄分析和包
在分析標識符的過程中,require()通過分析副檔名之後,可能沒有尋找到對應檔案,但卻得到一個目錄,此時Node會將目錄當做一個包來處理。
在這個過程中,Node對CommonJS包規範進行了一定程度的支援。首先,Node在目前的目錄下尋找package.json(CommonJS包規範定義的包描述檔案),通過JSON.parse()解析出包描述對象,從中取出main屬性指定的檔案名稱進行定位。如果檔案名稱缺少副檔名,將會進入副檔名分析的步驟。而如果main屬性指定的檔案名稱錯誤,或者壓根沒有package.json檔案,Node會將index當做預設檔案名稱,然後依次尋找index.js、index.node、index.json。
如果在目錄分析的過程中沒有定位成功任何檔案,則自訂模組進入下一個模組路徑進行尋找。如果模組路徑數組都被遍曆完畢,依然沒有尋找到目標檔案,則會拋出尋找失敗的異常。
3.模組編譯
在Node中,每個檔案模組都是一個對象,它的定義如下:
function Module(id, parent) { this.id = id; this.exports = {}; this.parent = parent; if (parent && parent.children) { parent.children.push(this); } this.filename = null; this.loaded = false; this.children = [];} 編譯和執行是引入檔案模組的最後一個階段。定位到具體的檔案後,Node會建立一個模組對象,然後根據路徑載入並編譯。對於不同的副檔名,其載入方法也有所不同,具體如下所示。
? .js檔案。
通過fs模組同步讀取檔案後編譯執行。
? .node檔案。
這是用C/C++編寫的擴充檔案,通過dlopen()方法載入最後編譯產生的檔案。
? .json檔案。
通過fs模組同步讀取檔案後,用JSON.parse()解析返回結果。
? 其餘副檔名檔案。
它們都被當做.js檔案載入。
每一個編譯成功的模組都會將其檔案路徑作為索引緩衝在Module._cache對象上,以提高二次引入的效能。
JavaScript模組的編譯
回到CommonJS模組規範,我們知道每個模組檔案中存在著require、exports、module這3個變數,但是它們在模組檔案中並沒有定義,那麼從何而來呢?甚至在Node的API文檔中,我們知道每個模組中還有__filename、__dirname這兩個變數的存在,它們又是從何而來的呢?如果我們把直接定義模組的過程放諸在瀏覽器端,會存在汙染全域變數的情況。
事實上,在編譯的過程中,Node對擷取的JavaScript檔案內容進行了頭尾封裝。在頭部添加了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n});。一個正常的JavaScript檔案會被封裝成如下的樣子:
(function (exports, require, module, __filename, __dirname) {
var math = require(‘math‘);
exports.area = function (radius) {
return Math.PI * radius * radius;
};
});
這樣每個模組檔案之間都進行了範圍隔離。封裝之後的代碼會通過vm原生模組的runInThisContext()方法執行(類似eval,只是具有明確上下文,不汙染全域),返回一個具體的function對象。最後,將當前模組對象的exports屬性、require()方法、module(模組對象自身),以及在檔案定位中得到的完整檔案路徑和檔案目錄作為參數傳遞給這個function()執行。
3.包和NPM在模組之外,包和NPM則是將模組聯絡起來的一種機制。
CommonJS的包規範的定義其實也十分簡單,它由包結構和包描述檔案兩個部分組成,前者用於組織包中的各種檔案,後者則用於描述包的相關資訊,以供外部讀取分析。
1.包結構包實際上是一個封存檔案,即一個目錄直接打包為.zip或tar.gz格式的檔案,安裝後解壓還原為目錄。完全符合CommonJS規範的包目錄應該包含如下這些檔案。
- package.json:包描述檔案。
- bin:用於存放可執行二進位檔案的目錄。
- lib:用於存放JavaScript代碼的目錄。
- doc:用於存放文檔的目錄。
- test:用於存放單元測試用例的代碼。
2.包描述檔案包描述檔案用於表達非代碼相關的資訊,它是一個JSON格式的檔案——package.json,位於包的根目錄下,是包的重要組成部分。而NPM的所有行為都與包描述檔案的欄位息息相關。
這個可以看看NPM官網對package.json的定義規範。
可以通過npm adduser, npm publish把自己的package上傳到npm倉庫。
三、題外話: AMD、CMD、相容多種模組規範的類庫1. AMD是CommonJS模組規範的一個延伸,它的模組定義如下:
define(id?, dependencies?, factory);
2.CMD
3.相容為了讓同一個模組可以運行在前後端,在寫作過程中需要考慮相容前端也實現了模組規範的環境。為了保持前後端的一致性,類庫開發人員需要將類庫程式碼封裝裝在一個閉包內。以下代碼示範如何將hello()方法定義到不同的運行環境中,它能夠相容Node、AMD、CMD以及常見的瀏覽器環境中:
相關文章: http://ifandelse.com/its-not-hard-making-your-library-support-amd-and-commonjs/
Node.js中的模組機制