5 图看懂 Node 模块加载原理
一.模块类型
Node.js 默认支持 2 种模块:
核心模块(Core Modules):编译成二进制,其源码位于lib/目录下
- 文件模块(File Modules):包括 JavaScript 文件(.js)、JSON 文件(.json)、C++扩展文件(.node)
由易到难,先看最常打交道的 JS 模块
二.JS 模块
js module
注意一个细节,是在加载&执行模块文件前会先缓存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加载,具体流程如下:
json module
除加载&执行方式不同外,与 JS 模块的加载流程完全一致
四.C++扩展模块
与 JS、JSON 模块相比,C++扩展模块(.node)的加载过程与 C++层关系更密切:
addon module
JS 层的处理流程到process.dlopen()为止,实际加载、执行、以及扩展模块暴露出的属性/方法如何传入 JS 运行时都是由 C++层来完成的:
addon module cpp
关键在于通过dlopen()/uv_dlopen加载 C++动态链接库(即.node文件)。相关 Node.js 源码见(Node v14.0.0):
模块加载:DLOpen、DLib::Open、DLib::Close
模块自注册:NODE_MODULE宏、node_module_register
- N-API:napi_module_register_by_symbol
之所以能够从外部取到扩展模块的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++模块)
core module
其中比较有意思的是 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#L24NativeModuleLoader::NativeModuleLoader() : config_(GetConfig()) { // 该函数的实现不在源码中,而是位于编译生成的 node_javascript.cc 中 LoadJavaScriptSource();}
核心 C++模块注册
所有核心模块依赖的 C++部分代码末尾都有一行注册代码,例如:
// src/node_file.ccNODE_MODULE_CONTEXT_AWARE_INTERNAL(fs, node::fs::Initialize)// src/timers.ccNODE_MODULE_CONTEXT_AWARE_INTERNAL(timers, node::Initialize)// src/js_stream.ccNODE_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
参考资料
Modules
更多相关文章
- 模块_Haskell笔记2
- MyBatis 延迟加载、一二级缓存、架构设计的面试题(常问,重点了解)
- 每日学习-ansible yum模块
- 每日学习-ansible firewalld模块
- Spring【DAO模块】知识要点
- Spring【AOP模块】就这么简单
- 图书管理系统【用户、购买、订单模块、添加权限】
- 图书管理系统【部署开发环境、解决分类、图书、前台页面模块】