前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >聊聊 Node.js 的模块机制

聊聊 Node.js 的模块机制

作者头像
theanarkh
发布2021-10-11 12:17:48
4650
发布2021-10-11 12:17:48
举报
文章被收录于专栏:原创分享原创分享

前言:模块机制是 Node.js 中非常重要的组成,模块机制使得我们可以以模块化的方式写代码,而不是全部代码都写到一个文件里。我们平时使用的比较多的通过 require 加载模块,但是我们可能不是很清楚 require 的实现原理,另外 Node.js 里存在多种模块类型,加载原理也不太一样,本文将会介绍 Node.js 模块机制以及实现原理。

1 模块机制的初始化和使用

1.1 注册 C++ 模块

在 Node.js 启动的时候,会通过 RegisterBuiltinModules 注册 C++ 模块。

代码语言:javascript
复制
void RegisterBuiltinModules() {  
 #define V(modname) _register_##modname();  
   NODE_BUILTIN_MODULES(V)  
 #undef V  }

NODE_BUILTIN_MODULES是一个C语言宏,宏展开后如下(省略类似逻辑)

代码语言:javascript
复制
voidRegisterBuiltinModules() {  
    #define V(modname) _register_##modname();  
      V(tcp_wrap)   
      V(timers)  
      ...其它模块  
    #undef V  }

再一步展开如下

代码语言:javascript
复制
void RegisterBuiltinModules() {  
  _register_tcp_wrap();  
  _register_timers();  
}

执行了一系列_register开头的函数,但是我们在Node.js源码里找不到这些函数,因为这些函数是在每个C++模块定义的文件里(.cc文件的最后一行)通过宏定义的。以tcp_wrap模块为例,看看它是怎么做的。文件tcp_wrap.cc的最后一句代码 NODE_MODULE_CONTEXT_AWARE_INTERNAL(tcp_wrap, node::TCPWrap::Initialize) 宏展开是

代码语言:javascript
复制
#define NODE_MODULE_CONTEXT_AWARE_INTERNAL(modname, regfunc)  \  
    NODE_MODULE_CONTEXT_AWARE_CPP(modname, 
                                  regfunc, 
                                  nullptr, 
                                  NM_F_INTERNAL)

继续展开

代码语言:javascript
复制
#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 开头的函数,在 Node.js 启动时,就会把这些函数逐个执行一遍。我们继续看一下这些函数都做了什么,在这之前,我们要先了解一下Node.js中表示 C++ 模块的数据结构。

代码语言:javascript
复制
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 开头的函数调了 node_module_register,并传入一个 node_module 数据结构,所以我们看一下node_module_register 的实现

代码语言:javascript
复制
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++ 内置模块的 flag 是 NM_F_INTERNAL,所以会执行第一个if的逻辑,modlist_internal 类似一个头指针。if 里的逻辑就是头插法建立一个单链表。

1.2 初始化模块加载器

注册完 C++ 模块后,接着初始化模块加载器。

代码语言:javascript
复制
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()};

  // 执行 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 的内容,并且封装到一个函数中,这个函数如下

代码语言:javascript
复制
function (process, getLinkedBinding, getInternalBinding, primordials) {
    // internal/bootstrap/loaders.js 的内容}

然后执行这个参数,并传入四个实参。我们看看 internal/bootstrap/loaders.js 执行后返回了什么。

代码语言:javascript
复制
const loaderExports = {
  // 加载 C++ 模块
  internalBinding,
  // 原生 JS 模块管理器
  NativeModule,
  // 原生 JS 加载器
  require: nativeModuleRequire
};

返回了两个模块加载器和一个模块管理器。接着 Node.js 把他们存起来,后续使用。

代码语言:javascript
复制
// 保存函数执行的返回结果
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 执行用户 JS

Node.js 初始化完毕后最终会通过以下代码执行用户的代码。

代码语言:javascript
复制
StartExecution(env, "internal/main/run_main_module")

看看 StartExecution。

代码语言:javascript
复制
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 函数
      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, &parameters, &arguments));}

传入了两个加载器,然后执行 run_main_module.js。核心代码如下

代码语言:javascript
复制
require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);

Module.runMain 的代码如下

代码语言:javascript
复制
function executeUserEntryPoint(main = process.argv[1]) {
  Module._load(main, null, true);}

最终通过 _load 完成用户代码的加载和执行,下面我们具体分析各种加载器。

2 模块加载的实现

我们平时都是通过 require 加载模块,require 帮我们处理一切,其实 Node.js 中有很多种类型的模块,下面我们逐个介绍。

2.1 JSON 模块

代码语言:javascript
复制
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 模块的实现很简单,读取文件的内容,解析一下就可以了。

2.2 用户 JS 模块

我们看到为什么在写代码的时候可以直接使用 require 函数,不是因为 require 是全局变量,而是我们写的代码会被封装到一个函数里执行,require 和 module.exports 等变量都是函数的形参,在执行我们代码时, Node.js 会传入实参,所以我们就可以使用这些变量了。require 函数可以加载用户自定义的 JS,也可以加载原生 JS,比如net,不过 Node.js 会优先查找原生 JS。

2.3 原生 JS 模块

原生 JS 模块和用户 JS 模块的加载原理是类似的,但是也有些不一样的地方,我们看到执行原生 JS 模块代码时,传入的实参和加载用户 JS 时是不一样的。首先 require 变量的值是一个原生 JS 模块加载器,所以原生 JS 模块里通过 require 只能加载 原生 JS 模块。另外还有另一个实参也需要关注,那就是 internalBinding,internalBinding 用于加载 C++ 模块,所以在原生 JS 里可以通过 internalBinding 加载 C++模块。

2.4 C++ 模块

2.5 Addon 模块

后记:模块机制在任何语言里都是非常基础且重要的部分,深入理解 Node.js 的模块机制原理,我们知道 require 的时候到时候发生了什么,如果你对模块加载的具体实现感兴趣,可以去阅读 Node.js 的源码,也可以看一下 https://github.com/theanarkh/js_runtime_loader 这个仓库。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-09-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 模块机制的初始化和使用
  • 1.1 注册 C++ 模块
  • 1.2 初始化模块加载器
  • 1.3 执行用户 JS
  • 2 模块加载的实现
  • 2.1 JSON 模块
  • 2.2 用户 JS 模块
  • 2.3 原生 JS 模块
  • 2.4 C++ 模块
  • 2.5 Addon 模块
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档