一、模塊類型
Node.js 默認支持 2 種模塊:
-
核心模塊(Core Modules):編譯成二進制,其源碼位於 lib/ 目錄下
-
文件模塊(File Modules):包括 JavaScript 文件(
.js)、JSON 文件(.json)、C++ 擴展文件(.node)
由易到難,先看最常打交道的 JS 模塊
二、JS 模塊
[caption id="attachment_2169" align="alignnone" width="496"]
js module[/caption]
注意一個細節,是在加載&執行模塊文件前會先緩存 module 實例,而不是之後才緩存,這是Node.js 能夠從容應對循環依賴的根本原因:
When there are circular require() calls, a module might not have finished executing when it is returned.
如果模塊加載過程中出現了循環引用,導致尚未加載完成的模塊被引用到,按照圖示的模塊加載流程也會命中緩存(而不至於進入死遞歸),即便此時的 module.exports 可能不完整(模塊代碼沒執行完,有些東西還沒掛上去)
P.S. 關於如何根據模塊標識找到對應模塊(入口)文件的絕對路徑,同名模塊加載優先級,以及相關 Node.js 源碼的解讀,見 [Node 模塊加載機制](/articles/node 模塊加載機制/)
三、JSON 模塊
類似於 JS 模塊,JSON 文件也可以作為模塊直接通過 require 加載,具體流程如下:
[caption id="attachment_2170" align="alignnone" width="541"]
json module[/caption]
除加載&執行方式不同外,與 JS 模塊的加載流程完全一致
四、C++ 擴展模塊
與 JS、JSON 模塊相比,C++ 擴展模塊(.node)的加載過程與 C++ 層關係更密切:
[caption id="attachment_2171" align="alignnone" width="532"]
addon module[/caption]
JS 層的處理流程到 process.dlopen() 為止,實際加載、執行、以及擴展模塊暴露出的屬性/方法如何傳入 JS 運行時都是由 C++ 層來完成的:
[caption id="attachment_2172" align="alignnone" width="625"]
addon module cpp[/caption]
關鍵在於通過 dlopen()/uv_dlopen 加載 C++ 動態鏈接庫(即 .node 文件)。相關 Node.js 源碼見(Node v14.0.0):
-
模塊加載:DLOpen、DLib::Open、DLib::Close
-
模塊自註冊:NODE_MODULE 宏、node_module_register
之所以能夠從外部取到擴展模塊的 module 實例,是因為擴展模塊有自註冊機制:
// 模塊註冊時
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast<struct node_module*>(m);
if (mp->nm_flags & NM_F_INTERNAL) {
mp->nm_link = modlist_internal;
modlist_internal = mp;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
// 將模塊實例掛到全局變量上,暴露出去
thread_local_modpending = mp;
}
}
// 加載模塊時
void DLOpen(const FunctionCallbackInfo<Value>& args) {
/* ...略去部分非關鍵代碼 */
const bool is_opened = dlib->Open();
// 加載動態鏈接庫後,讀全局變量,取出模塊實例
node_module* mp = thread_local_modpending;
thread_local_modpending = nullptr;
// 最後將 exports 和 module 傳給模塊入口函數,把模塊暴露出的屬性/方法��出來
if (mp->nm_context_register_func != nullptr) {
mp->nm_context_register_func(exports, module, context, mp->nm_priv);
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
}
}
P.S. 關於 C++ 擴展模塊開發、編譯、運行的詳細信息,見 [Node.js C++ 擴展入門指南](/articles/node-js-c 擴展入門指南/)
五、核心模塊
類似於 C++ 擴展模塊,核心模塊實現上大多依賴相應的下層 C++ 模塊(如文件 I/O、網絡請求、加密/解密等),只是通過 JS 封裝出面向用戶的上層接口(如 fs.writeFile、fs.writeFileSync 等)
本質上都是 C++ 類庫,最主要的區別在於核心模塊會被編譯到 Node.js 安裝包中(包括上層封裝的 JS 代碼,編譯時就已經鏈接到可執行文件中了),而擴展模塊需要在運行時動態加載
P.S. 關於 C++ 動態鏈接庫、靜態庫的更多信息,見 [Node.js C++ 擴展入門指南](/articles/node-js-c 擴展入門指南/#articleHeader1)
因此,與前幾種模塊相比,核心模塊的加載過程稍複雜些,分為 4 部分:
-
(預編譯階段)「編譯」JS 代碼
-
(啟動時)加載 JS 代碼
-
(啟動時)註冊 C++ 模塊
-
(運行時)加載核心模塊(包括 JS 代碼及其引用到的 C++ 模塊)
[caption id="attachment_2173" align="alignnone" width="625"]
core module[/caption]
其中比較有意思的是 JS2C 轉換與核心 C++ 模塊註冊兩部分
JS2C 轉換
通過編譯前的預處理,核心模塊的 JS 代碼部分被轉成了 C++ 文件(位於 ./out/Release/obj/gen/node_javascript.cc),進而打入可執行文件中:
NativeModule: a minimal module system used to load the JavaScript core modules found in lib/**/*.js and deps/**/*.js. All core modules are compiled into the node binary via node_javascript.cc generated by js2c.py, so they can be loaded faster without the cost of I/O. This class makes the lib/internal/*, deps/internal/* modules and internalBinding() available by default to core modules, and lets the core modules require itself via require('internal/bootstrap/loaders') even when this file is not written in CommonJS style.
(摘自 node/lib/internal/bootstrap/loaders.js)
生成的 node_javascript.cc 主要內容如下:
static const uint8_t internal_bootstrap_environment_raw[] = {
39,117,115,101, 32,115,116,114,105, 99,116, 39, 59, 10, 10, 47, 47, 32, 84,104,105,115, 32,114,117,110,115, 32,110,101,
99,101,115,115, 97,114,121, 32,112,114,101,112, 97,114, 97,116,105,111,110,115, 32,116,111, 32,112,114,101,112, 97,114
// ...
}
void NativeModuleLoader::LoadJavaScriptSource() {
source_.emplace("internal/bootstrap/environment", UnionBytes{internal_bootstrap_environment_raw, 374});
source_.emplace("internal/bootstrap/loaders", UnionBytes{internal_bootstrap_loaders_raw, 10110});
// ...
}
UnionBytes NativeModuleLoader::GetConfig() {
return UnionBytes(config_raw, 3030); // config.gypi
}
也就是說,翻遍源碼也找不到的 LoadJavaScriptSource 其實是在預編譯階段自動生成的:
// ref https://github.com/nodejs/node/blob/v14.0.0/src/node_native_module.cc#L24
NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) {
// 該函數的實現不在源碼中,而是位於編譯生成的 node_javascript.cc 中
LoadJavaScriptSource();
}
核心 C++ 模塊註冊
所有核心模塊依賴的 C++ 部分代碼末尾都有一行註冊代碼,例如:
// src/node_file.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)
// src/timers.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(timers, node::Initialize)
// src/js_stream.cc
NODE_MODULE_CONTEXT_AWARE_INTERNAL(js_stream, node::JSStream::Initialize)
NODE_MODULE_CONTEXT_AWARE_INTERNAL 宏展開之後是 node_module_register,將註冊過來的 C++ 模塊記錄到 modlist_internal 鏈表中:
extern "C" void node_module_register(void* m) {
struct node_module* mp = reinterpret_cast<struct node_module*>(m);
if (mp->nm_flags & NM_F_INTERNAL) {
// 記錄內部 C++ 模塊
mp->nm_link = modlist_internal;
modlist_internal = mp;
} else if (!node_is_initialized) {
// "Linked" modules are included as part of the node project.
// Like builtins they are registered *before* node::Init runs.
mp->nm_flags = NM_F_LINKED;
mp->nm_link = modlist_linked;
modlist_linked = mp;
} else {
thread_local_modpending = mp;
}
}
運行時通過 internalBinding 加載這些內置的 C++ 模塊
相關 Node.js 源碼見(Node v14.0.0):
-
JS 層模塊加載:Module._load、loadNativeModule、compileForInternalLoader、nativeModuleRequire、internalBinding
-
JS2C 轉換:tools/js2c.py、LoadJavaScriptSource、NativeModule.map、moduleIds、ModuleIdsGetter、GetModuleIds
-
核心 C++ 模塊註冊:NODE_MODULE_CONTEXT_AWARE_INTERNAL、node_module_register、InitModule
-
C++ 層模塊加載:internalBinding、getInternalBinding、FindModule、InitModule
暫無評論,快來發表你的看法吧