Node對CommonJS的模組規範
Node能夠以一種相對程度的的姿態出現,離不開CommonJS規範的影響。Node借鑑CommonJS的Modules規範實現了一套非常易用的模組系統,NPM對packages規範的完好支援使得Node應用在開發過程中事半功倍。
在Node中引用模組,需要經歷如下三個步驟。
1. 路徑分析
Node中的模組分為核心模組和檔案模組。
核心模組是由Node提供的模組,它們在Node原始碼的編譯過程中就編譯進了二進位制執行檔案,在Node程式啟動時,核心模組就被直接載入進記憶體中,所以在引用核心模組時,檔案定位和編譯執行這兩個步驟可以省略,並且在路徑分析中優先判斷,所以它的載入速度時最快的。通過require引用核心模組時,直接引用即可。如 require('http')
檔案模組是使用者編寫的模組,它是在執行時動態載入的,需要完整的路徑分析,檔案定位,編譯執行的過程,所以它的速度比核心模組慢。引用檔案模組的方式分為三種:
1.以.或..開始的相對路徑檔案模組。
2.以/開始的絕對路徑檔案模組。
3.非路徑形式的檔案模組(自定義模組)。
1,2兩種方法用於引用使用者自己編寫的模組,require會將路徑轉為真實路徑,並以真實路徑作為索引,將編譯執行的結果(物件)存放在快取中,由於指定了明確的檔案位置,其載入速度慢於核心模組,快於自定義模組。第3中方式用於引用下載的第三方模組,這類模組的查詢是最費時的。這裡有一個模組路徑的概念。自定義模組的查詢速度慢的原因就在於此。
/**
通過以下程式碼,可以看出模組路徑的生成規則如下:當前目錄下的node_modules目錄,父目錄下的node_modules目錄,沿路徑向上逐級遞迴,直到根目錄下的node_modules目錄。
*/
//a.js
console.log(module.paths)
//將打印出如下結果
[ 'H:\\Files\\qiuzhao\\please-offer\\node_modules','H:\\Files\\qiuzhao\\node_modules','H:\\Files\\node_modules','H:\\node_modules'
]
複製程式碼
1. require('../a.js' )
2. require('/a.js')
3. require('koa')
複製程式碼
2. 檔案定位
1) 副檔名:CommonJS規範允許在識別符號中不包含副檔名,這時候Node會按照.js,.json,.node的次序補足副檔名,依次嘗試。
2)目錄分析和包(自定義模組):在分析提供給require的識別符號的過程中,在副檔名的依次嘗試後,依然沒有得到對應的檔案,卻得到一個目錄,這在引用自定義模組並沿著模組路徑逐個進行查詢時經常會出現,此時Node會將目錄當做一個包來處理。這種情況下,Node首先會在當前目錄下查詢package.json(包描述檔案),通過JSON.parse()解析出物件後,從中取出main屬性指定的檔名進行定位,視情況而定會j副檔名的分析。如果main屬性指定的檔名錯誤或者根本就沒有package.json檔案,Node會將index當做預設檔名,然後進行副檔名的依次嘗試。如果在目錄分析的過程中沒有成功定位到任何檔案,則進入模組路徑的下一個路徑進行查詢,如果模組路徑陣列遍歷完畢仍未找到檔案,則丟擲錯誤。
3. 編譯執行
在Node中,每個檔案都是一個模組,每個模組都是一個物件,這個物件的定義如下:
function Module(id,parent){
this.id = id
this.exports = {}
this.parent = parent
if(parent&&parent.children){
parent.push(this)
}
this.filename = null
this.loaded = false
this.children = [] //當前模組引用的其他模組會儲存在這裡
}
複製程式碼
在成功定位到檔案後,首先Node會新建一個物件,然後會將檔案內容載入並編譯執行,並將模組的exports屬性返回給呼叫方。針對不同副檔名的檔案,有不同的載入方法,通過require.extensions
可以檢視系統以及支援的檔案載入方式。
1).js檔案:通過fs模組同步讀取檔案後編譯執行。
在編譯該型別的檔案時,Node會對獲取得檔案內容進行頭尾的包裝,在頭部新增(function(exports,require,module,__filename,__dirname){\n
,在尾部新增\n})
。一個正常的js檔案會被包裝成如下的樣子:
(function(exports,__dirname,__filename){
...
}) //從這裡可以看出,node對模組的實現,也借鑑了前端js經常使用的利用函式作用域還形成一個獨立的空間,以防汙染全域性作用域,這裡node包裝了這一過程。
複製程式碼
包裝之後的程式碼會通過vm原生模組的runInThisContext()方法執行(類似eval),返回一個具體的function物件(runInThisContext()的作用在這裡就是宣告一個函式),最後,將當前模組物件(別忘了Node在成功定位到檔案後,會首先建立一個module物件)的exports屬性,require方法,module本身,以及在之前兩步中得到的完整檔案路徑和檔案目錄作為引數傳遞給這個函式。這裡有一點經典的例子:
//當我們想為模組的輸出定義一個全新的物件時
//error
exports = {}
//right
module.exports = {}
//這樣做的原因時,exports和modlue.exports指向的是同一個物件,而exports={}這種方式,不會影響module.exports指向的物件。Node真正返回給呼叫者的是module.exports
複製程式碼
var val = 10
var chageVal = function(val){
val = 100
console.log(val)
}
changeVal(val) //100
console.log(val) //
/----------------------/
var obj = {
age:12
}
var changeName = function(obj){
obj = {
age:21
}
console.log(obj.age)
}
changeName(obj) //21
console.log(obj.age) //12
//出現這種現象的原因是,當呼叫函式時,傳入的是變數的副本。
複製程式碼
2).node檔案:這是使用C/C++編寫的擴充套件檔案,通過dlopen()方法載入最後編譯執行的結果。dlopen()方法在不同平臺下有不同的實現,通過libuv相容層封裝。實際上,.node的模組檔案不需要編譯,因為它是編寫C/C++模組之後編譯生成的,這裡只有載入和執行的過程,沒有編譯的過程。在執行的過程中,模組的exports物件與.node模組產生聯絡,然後返回給呼叫者。
3).json檔案:通過fs模組同步讀取檔案後,使用JSON.parse()解析後返回結果。 這種型別的檔案是三者中編譯最簡單的,Node利用fs模組同步讀取檔案內容後,呼叫JSON.parse()將其解析成物件,然後將其賦值給模組物件的exports,以供外部呼叫。
4).其他副檔名文件案:被當做.js檔案進行處理。
模組的快取
與前端瀏覽器會快取靜態指令碼檔案已提高效能一樣,Node對引用的模組都會進行快取,以減少二次引入時的開銷,不同之處在於,瀏覽器快取的是檔案,Node快取的是編譯和執行後的物件。require()方法對相同模組的二次載入一律採用快取優先的方式,這是第一優先順序的。每一個編譯成功的模組都會將其檔案路徑做為索引快取在Module._cache物件上,Module._cache會被賦值給require()方法的cache屬性,所以可以通過require.cache還檢視已經快取的模組。如果不想使用快取的模組,可以在被引用的模組內新增delete require.cache[module.filename]
。