本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。
前言:模块机制是 Node.js 中非常重要的组成,模块机制使得我们可以以模块化的方式写代码,而不是全部代码都写到一个文件里。我们平时使用的比较多的通过 require 加载模块,但是我们可能不是很清楚 require 的实现原理,另外 Node.js 里存在多种模块类型,加载原理也不太一样,本文将会介绍 Node.js 模块机制以及实现原理。
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();
- }
执行了一系列_register开头的函数,但是我们在Node.js源码里找不到这些函数,因为这些函数是在每个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)
继续展开
- 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++ 模块的数据结构。
- 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 的实现
- 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++ 模块后,接着初始化模块加载器。
- 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 的内容,并且封装到一个函数中,这个函数如下
- function (process, getLinkedBinding, getInternalBinding, primordials) {
- // internal/bootstrap/loaders.js 的内容
- }
然后执行这个参数,并传入四个实参。我们看看 internal/bootstrap/loaders.js 执行后返回了什么。
- const loaderExports = {
- // 加载 C++ 模块
- internalBinding,
- // 原生 JS 模块管理器
- NativeModule,
- // 原生 JS 加载器
- require: nativeModuleRequire
- };
返回了两个模块加载器和一个模块管理器。接着 Node.js 把他们存起来,后续使用。
- // 保存函数执行的返回结果
- 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 初始化完毕后最终会通过以下代码执行用户的代码。
- 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 函数
- 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));
- }
传入了两个加载器,然后执行 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 完成用户代码的加载和执行,下面我们具体分析各种加载器。
2 模块加载的实现
我们平时都是通过 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 模块的实现很简单,读取文件的内容,解析一下就可以了。
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 这个仓库。