一.模块类型
Node.js 默认支持 2 种模块:
核心模块(Core Modules):编译成二进制,其源码位于lib/目录下
文件模块(File Modules):包括 JavaScript 文件(
.js
)、JSON 文件(.json
)、C++扩展文件(.node
)
由易到难,先看最常打交道的 JS 模块
二.JS 模块
注意一个细节,是在加载&执行模块文件前会先缓存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 模块加载机制
三.JSON 模块
类似于 JS 模块,JSON 文件也可以作为模块直接通过require
加载,具体流程如下:
除加载&执行方式不同外,与 JS 模块的加载流程完全一致
四.C++扩展模块
与 JS、JSON 模块相比,C++扩展模块(.node
)的加载过程与 C++层关系更密切:
JS 层的处理流程到process.dlopen()
为止,实际加载、执行、以及扩展模块暴露出的属性/方法如何传入 JS 运行时都是由 C++层来完成的:
关键在于通过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++扩展入门指南
五.核心模块
类似于 C++扩展模块,核心模块实现上大多依赖相应的下层 C++模块(如文件 I/O、网络请求、加密/解密等),只是通过 JS 封装出面向用户的上层接口(如fs.writeFile
、fs.writeFileSync
等)
本质上都是 C++类库,最主要的区别在于核心模块会被编译到 Node.js 安装包中(包括上层封装的 JS 代码,编译时就已经链接到可执行文件中了),而扩展模块需要在运行时动态加载
P.S.关于 C++动态链接库、静态库的更多信息,见Node.js C++扩展入门指南
因此,与前几种模块相比,核心模块的加载过程稍复杂些,分为 4 部分:
(预编译阶段)“编译”JS 代码
(启动时)加载 JS 代码
(启动时)注册 C++模块
(运行时)加载核心模块(包括 JS 代码及其引用到的 C++模块)
其中比较有意思的是 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