聊聊 Node.js 的模塊機制
本文轉(zhuǎn)載自微信公眾號「編程雜技」,作者theanarkh。轉(zhuǎn)載本文請聯(lián)系編程雜技公眾號。
前言:模塊機制是 Node.js 中非常重要的組成,模塊機制使得我們可以以模塊化的方式寫代碼,而不是全部代碼都寫到一個文件里。我們平時使用的比較多的通過 require 加載模塊,但是我們可能不是很清楚 require 的實現(xiàn)原理,另外 Node.js 里存在多種模塊類型,加載原理也不太一樣,本文將會介紹 Node.js 模塊機制以及實現(xiàn)原理。
1 模塊機制的初始化和使用
1.1 注冊 C++ 模塊
在 Node.js 啟動的時候,會通過 RegisterBuiltinModules 注冊 C++ 模塊。
- void RegisterBuiltinModules() {
- #define V(modname) _register_##modname();
- NODE_BUILTIN_MODULES(V)
- #undef V
- }
NODE_BUILTIN_MODULES是一個C語言宏,宏展開后如下(省略類似邏輯)
- voidRegisterBuiltinModules() {
- #define V(modname) _register_##modname();
- V(tcp_wrap)
- V(timers)
- ...其它模塊
- #undef V
- }
再一步展開如下
- void RegisterBuiltinModules() {
- _register_tcp_wrap();
- _register_timers();
- }
執(zhí)行了一系列_register開頭的函數(shù),但是我們在Node.js源碼里找不到這些函數(shù),因為這些函數(shù)是在每個C++模塊定義的文件里(.cc文件的最后一行)通過宏定義的。以tcp_wrap模塊為例,看看它是怎么做的。文件tcp_wrap.cc的最后一句代碼 NODE_MODULE_CONTEXT_AWARE_INTERNAL(tcp_wrap, node::TCPWrap::Initialize) 宏展開是
- #define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc) \
- NODE_MODULE_CONTEXT_AWARE_CPP(modname,
- regfunc,
- nullptr,
- NM_F_INTERNAL)
繼續(xù)展開
- define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \
- static node::node_module _module = { \
- NODE_MODULE_VERSION, \
- flags, \
- nullptr, \
- __FILE__, \
- nullptr, \
- (node::addon_context_register_func)(regfunc), \
- NODE_STRINGIFY(modname), \
- priv, \
- nullptr}; \
- void _register_tcp_wrap() { node_module_register(&_module); }
我們看到每個C++模塊底層都定義了一個 _register 開頭的函數(shù),在 Node.js 啟動時,就會把這些函數(shù)逐個執(zhí)行一遍。我們繼續(xù)看一下這些函數(shù)都做了什么,在這之前,我們要先了解一下Node.js中表示 C++ 模塊的數(shù)據(jù)結(jié)構(gòu)。
- struct node_module {
- int nm_version;
- unsigned int nm_flags;
- void* nm_dso_handle;
- const char* nm_filename;
- node::addon_register_func nm_register_func;
- node::addon_context_register_func nm_context_register_func;
- const char* nm_modname;
- void* nm_priv;
- struct node_module* nm_link;
- };
我們看到 _register 開頭的函數(shù)調(diào)了 node_module_register,并傳入一個 node_module 數(shù)據(jù)結(jié)構(gòu),所以我們看一下node_module_register 的實現(xiàn)
- 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) {
- mp->nm_flags = NM_F_LINKED;
- mp->nm_link = modlist_linked;
- modlist_linked = mp;
- } else {
- thread_local_modpending = mp;
- }
- }
C++ 內(nèi)置模塊的 flag 是 NM_F_INTERNAL,所以會執(zhí)行第一個if的邏輯,modlist_internal 類似一個頭指針。if 里的邏輯就是頭插法建立一個單鏈表。
1.2 初始化模塊加載器
注冊完 C++ 模塊后,接著初始化模塊加載器。
- MaybeLocal<Value> Environment::BootstrapInternalLoaders() {
- EscapableHandleScope scope(isolate_);
- // 形參
- std::vector<Local<String>> loaders_params = {
- process_string(),
- FIXED_ONE_BYTE_STRING(isolate_, "getLinkedBinding"),
- FIXED_ONE_BYTE_STRING(isolate_, "getInternalBinding"),
- primordials_string()};
- // 實參
- std::vector<Local<Value>> loaders_args = {
- process_object(),
- NewFunctionTemplate(binding::GetLinkedBinding)
- ->GetFunction(context())
- .ToLocalChecked(),
- NewFunctionTemplate(binding::GetInternalBinding)
- ->GetFunction(context())
- .ToLocalChecked(),
- primordials()};
- // 執(zhí)行 internal/bootstrap/loaders.js
- Local<Value> loader_exports;
- if (!ExecuteBootstrapper(
- this, "internal/bootstrap/loaders", &loaders_params, &loaders_args)
- .ToLocal(&loader_exports)) {
- return MaybeLocal<Value>();
- }
- // ...
- }
ExecuteBootstrapper 會讀取 internal/bootstrap/loaders.js 的內(nèi)容,并且封裝到一個函數(shù)中,這個函數(shù)如下
- function (process, getLinkedBinding, getInternalBinding, primordials) {
- // internal/bootstrap/loaders.js 的內(nèi)容
- }
然后執(zhí)行這個參數(shù),并傳入四個實參。我們看看 internal/bootstrap/loaders.js 執(zhí)行后返回了什么。
- const loaderExports = {
- // 加載 C++ 模塊
- internalBinding,
- // 原生 JS 模塊管理器
- NativeModule,
- // 原生 JS 加載器
- require: nativeModuleRequire
- };
返回了兩個模塊加載器和一個模塊管理器。接著 Node.js 把他們存起來,后續(xù)使用。
- // 保存函數(shù)執(zhí)行的返回結(jié)果
- Local<Value> loader_exports;
- if (!ExecuteBootstrapper(
- this, "internal/bootstrap/loaders", &loaders_params, &loaders_args)
- .ToLocal(&loader_exports)) {
- return MaybeLocal<Value>();
- }
- Local<Object> loader_exports_obj = loader_exports.As<Object>();
- // 獲取 C++ 模塊加載器
- Local<Value> internal_binding_loader = loader_exports_obj->Get(context(), internal_binding_string())
- .ToLocalChecked();
- // 保存 C++ 模塊加載器set_internal_binding_loader(internal_binding_loader.As<Function>());
- // 獲取原生 JS 加載器
- Local<Value> require = loader_exports_obj->Get(context(), require_string()).ToLocalChecked();
- // 保存原生 JS 加載器set_native_module_require(require.As<Function>());
1.3 執(zhí)行用戶 JS
Node.js 初始化完畢后最終會通過以下代碼執(zhí)行用戶的代碼。
- StartExecution(env, "internal/main/run_main_module")
看看 StartExecution。
- MaybeLocal<Value> StartExecution(Environment* env, const char* main_script_id) {
- EscapableHandleScope scope(env->isolate());
- CHECK_NOT_NULL(main_script_id);
- std::vector<Local<String>> parameters = {
- env->process_string(),
- // require 函數(shù)
- env->require_string(),
- env->internal_binding_string(),
- env->primordials_string(),
- FIXED_ONE_BYTE_STRING(env->isolate(), "markBootstrapComplete")};
- std::vector<Local<Value>> arguments = {
- env->process_object(),
- // 原生 JS 和 C++ 模塊加載器
- env->native_module_require(),
- env->internal_binding_loader(),
- env->primordials(),
- env->NewFunctionTemplate(MarkBootstrapComplete)
- ->GetFunction(env->context())
- .ToLocalChecked()};
- return scope.EscapeMaybe(
- ExecuteBootstrapper(env, main_script_id, ¶meters, &arguments));
- }
傳入了兩個加載器,然后執(zhí)行 run_main_module.js。核心代碼如下
- require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
Module.runMain 的代碼如下
- function executeUserEntryPoint(main = process.argv[1]) {
- Module._load(main, null, true);
- }
最終通過 _load 完成用戶代碼的加載和執(zhí)行,下面我們具體分析各種加載器。
2 模塊加載的實現(xiàn)
我們平時都是通過 require 加載模塊,require 幫我們處理一切,其實 Node.js 中有很多種類型的模塊,下面我們逐個介紹。
2.1 JSON 模塊
- Module._extensions['.json'] = function(module, filename) {
- const content = fs.readFileSync(filename, 'utf8');
- try {
- module.exports = JSONParse(stripBOM(content));
- } catch (err) {
- err.message = filename + ': ' + err.message;
- throw err;
- }
- };
JSON 模塊的實現(xiàn)很簡單,讀取文件的內(nèi)容,解析一下就可以了。
2.2 用戶 JS 模塊
我們看到為什么在寫代碼的時候可以直接使用 require 函數(shù),不是因為 require 是全局變量,而是我們寫的代碼會被封裝到一個函數(shù)里執(zhí)行,require 和 module.exports 等變量都是函數(shù)的形參,在執(zhí)行我們代碼時, Node.js 會傳入實參,所以我們就可以使用這些變量了。require 函數(shù)可以加載用戶自定義的 JS,也可以加載原生 JS,比如net,不過 Node.js 會優(yōu)先查找原生 JS。
2.3 原生 JS 模塊
原生 JS 模塊和用戶 JS 模塊的加載原理是類似的,但是也有些不一樣的地方,我們看到執(zhí)行原生 JS 模塊代碼時,傳入的實參和加載用戶 JS 時是不一樣的。首先 require 變量的值是一個原生 JS 模塊加載器,所以原生 JS 模塊里通過 require 只能加載 原生 JS 模塊。另外還有另一個實參也需要關(guān)注,那就是 internalBinding,internalBinding 用于加載 C++ 模塊,所以在原生 JS 里可以通過 internalBinding 加載 C++模塊。
2.4 C++ 模塊
2.5 Addon 模塊
后記:模塊機制在任何語言里都是非?;A(chǔ)且重要的部分,深入理解 Node.js 的模塊機制原理,我們知道 require 的時候到時候發(fā)生了什么,如果你對模塊加載的具體實現(xiàn)感興趣,可以去閱讀 Node.js 的源碼,也可以看一下 https://github.com/theanarkh/js_runtime_loader 這個倉庫。