Node.js 的模塊系統

分類:技術 時間:2017-01-13

Node 的模塊系統是借鑒 CommonJS 的 Modules 規范實現的,因此,下面我們需要先了解 CommonJS 的 Modules 規范。

CommonJS 的 Modules 規范

CommonJS 對模塊的定義非常簡單,主要分為 模塊引用、模塊定義和模塊標識三個部分。

  1. 模塊引用 - require() 方法
  2. 模塊定義 - module.exports 對象
  3. 模塊標識 - 傳遞給 require() 方法的參數

通過 CommonJS 的這套導出和引入機制,用戶不必再考慮變量污染的問題。

Node 的模塊實現

Node 中的模塊分為兩類:

  1. Node 提供的模塊 - 核心模塊
  2. 用戶編寫的模塊 - 文件模塊

核心模塊:

  1. 在 Node 源代碼的編譯過程中,編譯進了二進制文件
  2. 在 Node 進程啟動時,部分核心模塊被直接加載近內存

文件模塊:

  1. 運行時動態加載,需要完整的路徑分析、文件定位、編譯執行過程

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 的次序補充擴展名,依次嘗試

目錄、包的處理:如果通過標識符沒找到對應文件,但是找到了同名的一個目錄

  1. 首先,Node 會在該目錄下查找 package.json 文件,從中取出 main 屬性指定的文件名對應
  2. 如果 package.jsonmain 屬性指定的文件名錯誤或者是直接沒有 package.json 文件,Node 會將 index 作為默認文件名
  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 文件:通過 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 代碼

  1. Node 采用 V8 附帶的 js2c.py 工具將所有內置的 JavaScript 代碼轉換成 C 里的數組
  2. 在這個過程中,JavaScript 代碼以字符串的形式存儲在 node 命名空間中,是不可執行的
  3. 在啟動 Node 進程時,JavaScript 代碼直接加載進內存中
  4. 在加載的過程中,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


ads
ads

相關文章
ads

相關文章

ad