5 图看懂 Node 模块加载原理

一.模块类型
Node.js 默认支持 2 种模块:

  • 核心模块(Core Modules):编译成二进制,其源码位于lib/目录下

  • 文件模块(File Modules):包括 JavaScript 文件(.js)、JSON 文件(.json)、C++扩展文件(.node)

由易到难,先看最常打交道的 JS 模块

二.JS 模块
5 图看懂 Node 模块加载原理

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加载,具体流程如下:

5 图看懂 Node 模块加载原理

json module

除加载&执行方式不同外,与 JS 模块的加载流程完全一致

四.C++扩展模块
与 JS、JSON 模块相比,C++扩展模块(.node)的加载过程与 C++层关系更密切:

5 图看懂 Node 模块加载原理

addon module

JS 层的处理流程到process.dlopen()为止,实际加载、执行、以及扩展模块暴露出的属性/方法如何传入 JS 运行时都是由 C++层来完成的:

5 图看懂 Node 模块加载原理

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++模块)

5 图看懂 Node 模块加载原理

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

更多相关文章

  1. 模块_Haskell笔记2
  2. MyBatis 延迟加载、一二级缓存、架构设计的面试题(常问,重点了解)
  3. 每日学习-ansible yum模块
  4. 每日学习-ansible firewalld模块
  5. Spring【DAO模块】知识要点
  6. Spring【AOP模块】就这么简单
  7. 图书管理系统【用户、购买、订单模块、添加权限】
  8. 图书管理系统【部署开发环境、解决分类、图书、前台页面模块】

随机推荐

  1. 对于有3亿多万条记录的MySQL表,有哪些优化
  2. centos7 移动mysql5.7.19 数据存储位置
  3. MySQL5 LOAD DATA 的使用
  4. 【数据库管理工具】Navicat安装及使用教
  5. 电商平台的搭建(SpringMVC+SpringSecurity
  6. mysql数据库100万条数据插入采用jdbc的各
  7. MySQL内核解析:Innodb页面存储结构-1
  8. 在分组之前使用orderby来自两个不同的表
  9. MySQL中一些查看事务和锁情况的常用语句
  10. Netbeans6.1+JSF/VJSF/+JPA+MYSQL=酷炫快