-
1. 模塊機制
- 1.1 commonjs規範
-
1.2 node的模塊實現(node中引入模塊的過程)
- 1.2.1 優先從緩存中加載
- 1.2.2 路徑分析
- 1.2.3 文件定位
- 1.2.4 模塊編譯
-
1.3 核心模塊
- 1.3.1 js核心模塊的編譯過程
- 1.3.2 c/c++核心模塊的編譯過程
- 1.3.3 核心模塊的引入流程
- 1.4 c/c++擴展模塊
1. 模塊機制
1.1 commonjs規範
- CommonJS規範為JavaScript制定了一個美好的願景——希望JavaScript能夠在任何地方運行。
- CommonJS對模塊的定義十分簡單,主要分為模塊引用、模塊定義和模塊標識3個部分。
- 模塊引用:通過require方法引入模塊
var math = require('math');
- 模塊定義:通過exports對象導出當前模塊的方法或變量(exports是module的屬性)
exports.add = function() {}
-
模塊標識:就是傳遞給require方法的參數,可以是以下幾種形式(可以沒有文件後綴名.js):
- 符合小駝峯命名的字符串
- 以.、..開頭的相對路徑
- 絕對路徑
1.2 node的模塊實現(node中引入模塊的過程)
node在實現中並非完全按照connonjs規範,而是對模塊規範進行了一定的取捨,同時增加了少許自身的特性。
1. 在node中引入模塊需要經歷如下3個步驟:
- 路徑分析
- 文件定位
- 編譯執行
2. node中,模塊分為以下兩類:
-
核心模塊:node提供的模塊
- 核心模塊在node源代碼編譯過程中,編譯進了二進制執行文件
- 在node進程啓動時,部分核心模塊就被直接加載到了內存中(所以這部分核心模塊在引入時,文件定位和編譯執行這兩部分是可以省略的,且在路徑分析中優先判斷,加載速度最快)
-
文件模塊:用户編寫的模塊
- 需要在運行時動態加載,需要完整的路徑分析、文件定位和編譯執行。速度較慢
以下為詳細的模塊加載過程:
1.2.1 優先從緩存中加載
- node對引入過的模塊都會進行緩存,以減少二次引入時的開銷(和瀏覽器緩存的不同之處在於:瀏覽器僅僅緩存文件,而node緩存的是編譯和執行後的對象)
- 對核心模塊的緩存檢查優先於文件模塊
1.2.2 路徑分析
因為標識符有幾種形式,所以對於不同的標識符,模塊的查找和定位有着不同程度的差異。模塊分類如下:
- 核心模塊:如http、fs、path等
- 以.或..開始的相對路徑文件模塊
- 以/開始的絕對路徑文件模塊
- 非路徑形式的文件模塊,如自定義的文件模塊
1. 核心模塊
核心模塊的優先級僅次於緩存加載,它在node源代碼編譯過程中被編譯成了二進制文件,加載速度最快。
2. 路徑形式的文件模塊
require會將路徑轉化為真實路徑,並以真實路徑作為索引,將編譯執行後的結果放到緩存中,以使二次加載更快。
3. 自定義模塊
node會逐個嘗試模塊路徑中的路徑,直到找到目標文件為止。
- 模塊路徑概念:模塊路徑是node在定位文件模塊的具體文件時指定的查找策略,具體表現為一個路徑組成的數組(可通過module.paths輸出)
-
模塊路徑的生成規則:
- 當前文件目錄下的node_modules目錄
- 父目錄下的node_modules目錄
- 父目錄的父目錄下的node_modules目錄
- 沿路徑向上逐級遞歸,直到根目錄下的node_modules目錄
1.2.3 文件定位
- 文件擴展名的分析
- 目錄和包的處理
1. 文件擴展名的分析:當傳遞給require的標識符不包括文件擴展名時,node會按照以下次序補足擴展名,依次嘗試:
- .js
- .json
- .node
在嘗試的過程中,node會調用fs模塊同步阻塞式的判斷文件是否存在。因為node是單線程的,所以這裏是一個會引發性能問題的地方。(解決:在引入.json和.node文件時,加上擴展名)
2. 目錄和包的處理:require通過分析文件擴展名後,可能得到的是一個目錄,此時node會將目錄當做一個包來處理。
- 首先,node在當前目錄下查找package.json。通過json.parse解析出包描述對象,從中取出main屬性指定的文件名進行分析,若缺少擴展名,則會進入擴展名分析的步驟。
- 若main指定的文件名錯誤,或者沒有package.json,那麼node會將index作為文件名。然後依次查找index.js、index.json、index.node
- 如果在目錄分析的過程中沒有成功定位到任何文件,則自定義模塊會進入到下一個模塊路徑繼續查找。若遍歷完模塊路徑後仍然沒有,則會拋出查找失敗的異常。
1.2.4 模塊編譯
定位到具體的文件後,node會新建一個模塊對象,然後根據路徑載入和編譯。對於不同的擴展名,其載入方式如下:
- .js文件:通過fs模塊同步讀取文件後編譯執行
- .node文件:這是用c/c++編寫的擴展文件,通過dlopen加載最後編譯生成的文件
- .json文件:通過fs模塊同步讀取文件後,用Json.parse解析返回結果
- 其餘擴展名文件:都被當做.js文件載入
注:每個編譯成功的模塊,都會將其文件路徑作為索引緩存在Module._cache對象上,以提高二次引入的性能
1. js模塊的編譯
node對獲取的js文件進行了頭尾的包裝,包裝後的代碼會通過vm原生模塊的runInThisContext()執行
(function(exports, requiure, module, __filename, __dirname){
...
}
2. c/c++模塊的編譯
node調用process.dlopen()進行加載和執行,(dlope在windows和*nux平台通過libuv進行了兼容).node模塊並不需要編譯,因為他是編寫c/c++模塊之後編譯生成的。
3. json文件的編譯
- 通過fs模塊同步讀取到json文件的內容
- 通過JSON.parse()得到對象
- 複製給module.exports
1.3 核心模塊
- c/c++編寫(存放在node的src目錄)
- js編寫(存放在lib目錄)
1.3.1 js核心模塊的編譯過程
- 轉存為c/c++代碼:node通過v8附帶的js2c.py工具,將所有內置的js代碼轉換成c++裏的數組,生成node_natives.h頭文件。(在這個過程中,js代碼以字符串的形式存儲在node命名空間中。在啓動node進程時,js代碼直接加載進內存中)
-
編譯js核心模塊:也經歷了頭尾的包裝過程(與文件模塊的區別在於:獲取源碼的方式和緩存執行結果的位置)
- 核心模塊從內存中加載
- 編譯成功的模塊緩存到NativeModule._cache對象; 文件模塊則緩存到Module._cache對象
1.3.2 c/c++核心模塊的編譯過程
- 內建模塊:全部由c/c++編寫(如buuffer、fs、os等)
- c/c++完成核心部分,js實現封裝
1. 內建模塊
- 每一個內建模塊在定義之後,都通過NODE_MODULE宏將模塊定義到node命名空間中。
- nodex_extensions.h頭文件將這些散列的內建模塊統一放到了node_module_list數組
- node提供了get_builtin_module()從node_module_list中取出內建模塊
- 內建模塊的導出:通過process.binding()來加載內建模塊(process是node在啓動時生成的全局變量)
1.3.3 核心模塊的引入流程
- NODE_MODULE(node_os, reg_func)
- get_builtin_module('node_os')
- process.binding('os')
- NativeModule.require('os')
- require('os')
1.4 c/c++擴展模塊
- 模塊編寫:
普通的擴展模塊和內建模塊的區別在於:無需將源代碼編譯進node,而是通過dlopen()動態加載
- 模塊編譯:通過gpy工具
-
模塊加載:通過require()加載
-
對於.node文件使用process.dlopen()加載
- 通過uv_dlopen()打開動態鏈接庫
- 通過uv_dlsym()找到動態鏈接庫中通過NODE_MODULE宏定義的方法地址
(注:以上兩個方法都是在libuv庫中實現的。在不同的操作系統下分別調用不同的方法來分別加載.node在該操作系統下對應的文件.so和.dll)
-