Node 的模塊系統是借鑒 CommonJS 的 Modules 規范實現的,因此,下面我們需要先了解 CommonJS 的 Modules 規范。
CommonJS 的 Modules 規范
CommonJS 對模塊的定義非常簡單,主要分為 模塊引用、模塊定義和模塊標識三個部分。
-
模塊引用 -
require()
方法 -
模塊定義 -
module.exports
對象 -
模塊標識 - 傳遞給
require()
方法的參數
通過 CommonJS 的這套導出和引入機制,用戶不必再考慮變量污染的問題。
Node 的模塊實現
Node 中的模塊分為兩類:
- Node 提供的模塊 - 核心模塊
- 用戶編寫的模塊 - 文件模塊
核心模塊:
- 在 Node 源代碼的編譯過程中,編譯進了二進制文件
- 在 Node 進程啟動時,部分核心模塊被直接加載近內存
文件模塊:
- 運行時動態加載,需要完整的路徑分析、文件定位、編譯執行過程
Node 緩存
Node 對引入過的模塊都會進行緩存,Node 緩存的是編譯執行之后的對象,require() 方法對相同模塊的加載一律采用緩存優先的方式,這是第一優先級。
路徑分析和文件定位
模塊標識符分析
模塊標識符,就是傳入 require()
方法的參數,對于不同的模塊標識符,查找和定位的方式是不一樣的。
模塊標識在 Node 中主要分為以下幾類:
- 核心模塊(http、fs、path .etc),加載優先級僅次于緩存加載,其加載過程最快
-
.
或..
開頭的相對路徑,都會被當做文件模塊處理,require()
方法會將路徑轉換為真實路徑,并以真實的路徑作為索引,將編譯執行后的結果存放到緩存中 -
/
開始的絕對路徑,同上 - 非路徑開始的文件模塊,它不是核心模塊,但是又不寫成路徑的形式,這類的查找是最費時的,詳細的分析在下文
模塊路徑
在了解自定義模塊的查找方式之前,需要先知道 模塊路徑
這個概念。
模塊路徑
是 Node
在定位文件模塊的具體文件是制定的查找策略,具體表現為一個路徑組成的數組。
模塊路徑
的生成規則如下:
-
當前目錄下的
node_modules
目錄 -
父目錄下的
node_modules
目錄 -
父目錄的父目錄下的
node_modules
目錄 -
一直到系統的根目錄下的
node_modules
目錄
在加載的過程中,Node 會逐個嘗試模塊路徑中的路徑,知道找到目標文件為止
文件定位
Node 在定位好文件之后,還需要做一些事情,包括:擴展名的分析以及目錄、包的處理
擴展名的分析:如果標識符不包含擴展名,Node 會按 .js
.json
.node
的次序補充擴展名,依次嘗試
目錄、包的處理:如果通過標識符沒找到對應文件,但是找到了同名的一個目錄
-
首先,Node 會在該目錄下查找
package.json
文件,從中取出main
屬性指定的文件名對應 -
如果
package.json
的main
屬性指定的文件名錯誤或者是直接沒有package.json
文件,Node 會將index
作為默認文件名 - 如果在目錄分析的過程中沒有定位成功任何的文件,則自定義模塊進入下一個模塊路徑進行搜索,如果路徑數組已經遍歷完了還沒找到目標文件則會拋出一個異常
模塊編譯
編譯和執行是引入文件模塊的最后一個階段,在 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
文件:通過dlopen()
方法加載最后編譯生成的文件 -
.json
文件:通過fs
模塊同步讀取文件后,通過JSON.parse()
解析返回結果 - 其余擴展名文件
Module._extensions
會被賦值給 require()
的 extensions
屬性,如果想對自定義的擴展名進行特殊的加載,可以通過類似 require.extension['.coffee']
擴展的方式來實現。
編譯
JavaScript 模塊的編譯
一個正常的 JavaScript 文件會被包裝成如下的樣子:
(function (exports, require, module, __filename, __dirname){ // 文件里本來的 js 代碼 })
module.exports
對象上的任何方法和屬性都可以被外部調用到。
C/C 模塊的編譯
Node
調用
process.dlopen()
方法進行加載和執行。
JSON 文件的編譯
Node 利用 fs
模塊同步讀取 .json
文件,調用 JSON.parse()
方法得到對象然后賦值給 module.exports
每一個編譯成功的模塊都會將其文件路徑作為索引緩存在 Module._chche
對象上。
核心模塊
Node 的核心模塊在編譯成可執行文件的過程中被編譯進了二進制文件。核心模塊分為 C/C 和 JavaScript 編寫的兩個部分
JavaScript 核心模塊的編譯過程
第一步:轉存為 C/C 代碼
-
Node 采用 V8 附帶的
js2c.py
工具將所有內置的 JavaScript 代碼轉換成 C 里的數組 -
在這個過程中,JavaScript 代碼以字符串的形式存儲在
node
命名空間中,是不可執行的 - 在啟動 Node 進程時,JavaScript 代碼直接加載進內存中
- 在加載的過程中,JavaScript 核心模塊經歷標識符分析后直接定位到內存中
第二步:編譯 JavaScript 核心模塊
與文件模塊有區別的地方在于:獲取源代碼的方式(核心模塊是從內存中加載的)和緩存( NativeModule._cache
)執行結果的位置。
C/C 核心模塊的編譯過程
由純 C/C 編寫的部分統稱為內建模塊,因為他它們通常不被用戶直接調用。Node 的 buffer、crypto、evals、fs、os 等模塊都是內建模塊。
… 這個坑先留著
內建模塊的導出
Node 在啟動時,會生成一個全局變量 process
,并提供 Binding()
方法來協助加載內建模塊。
前面提到的 JavaScript 核心文件被轉換為 C/C 數組存儲后,便是通過 process.binding('natives')
取出放置在 NativeModule._source
中的:
NativeModule._source = process.binding('natives')
Tags: Node.js C
文章來源:http://mertensming.github.io/2017/01/08/node-modul