当前位置: 首页 > 科技观察

浅谈Node.js的模块机制

时间:2023-03-19 16:45:40 科技观察

本文转载自微信公众号《编程杂技》,作者theanarkh。转载本文请联系编程杂技公众号。前言:模块机制是Node.js中非常重要的一个组成部分。模块机制允许我们以模块化的方式编写代码,而不是将所有代码都写到一个文件中。我们平时使用require加载模块的次数比较多,但是对于require的实现原理可能不是很清楚。另外,Node.js中的模块类型很多,加载原理也不一样。本文将介绍Node.js的模块机制和实现原理。1模块机制的初始化和使用1.1注册C++模块Node.js启动时,会通过RegisterBuiltinModules注册C++模块。voidRegisterBuiltinModules(){#defineV(modname)_register_##modname();NODE_BUILTIN_MODULES(V)#undefV}NODE_BUILTIN_MODULES为C语言宏,宏展开如下(类似逻辑略)voidRegisterBuiltinModules(){#defineV(modname)_register_##modname();V(tcp_wrap)V(timers)...其他模块#undefV}展开如下在Node.js源码中找不到这些函数,因为这些函数是在每个C++模块定义的文件(.cc文件的最后一行)中通过宏定义的。以tcp_wrap模块为例,看看它是如何实现的。文件tcp_wrap.cc的最后一句代码NODE_MODULE_CONTEXT_AWARE_INTERNAL(tcp_wrap,node::TCPWrap::Initialize)宏展开是#defineNODE_MODULE_CONTEXT_AWARE_INTERNAL(modname,regfunc)\NODE_MODULE_CONTEXT_AWARE_CPP(modname,regfunc,nullptr,NM_F_INTERNAL)继续展开defineNODE_MODULE_CONTEXT_AWARE_CPP(modname,regfunc,priv,flags)\staticnode::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++模块的数据结构。structnode_module{intnm_version;unsignedintnm_flags;void*nm_dso_handle;constchar*nm_filename;node::addon_register_funcnm_register_func;node::addon_context_register_funcnm_context_register_func;constchar*nm_modname;void*nm_priv;structnode_module*nm_link;};我们看到_register开头的函数调了node_module_register,并传入一个node_module数据结构,那么我们看一下node_module_register的实现;modlist_internal=mp;}elseif(!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里面的逻辑就是通过head插值的方式创建一个单向链表。1.2初始化模块加载器注册C++模块后,初始化模块加载器。MaybeLocalEnvironment::BootstrapInternalLoaders(){EscapableHandleScopescope(isolate_);//形参std::vector>loaders_params={process_string(),FIXED_ONE_BYTE_STRING(isolate_,"getLinkedBinding"),FIXED_ONE_BYTE_STRING(isolate_,"getInternalBinding"),primordials_string()};//实例std::vector>loaders_args={process_object(),NewFunctionTemplate(binding::GetLinkedBinding)->GetFunction(context()).ToLocalChecked(),NewFunctionTemplate(binding::GetInternalBinding)->GetFunction(context()).ToLocalChecked(),primordials()};//执行internal/bootstrap/loaders.jsLocalloader_exports;if(!ExecuteBootstrapper(this,"internal/bootstrap/loaders",&loaders_params,&loaders_args).ToLocal(&loader_exports)){returnMaybeLocal();}//...}ExecuteBootstrapper会读取internal/bootstrap/loaders.js的内容,并封装到一个函数中,这个函数如下函数(process,getLinkedBinding,getInternalBinding,primordials){//internal/bootstrap/loaders.jscontent}然后执行这个参数,传入四个实际参数,看看internal/bootstrap/loaders.js执行后返回的是什么。constloaderExports={//LoadC++moduleinternalBinding,//NativeJSmodulemanagerNativeModule,//NativeJSloaderrequire:nativeModuleRequire};返回两个模块加载器和一个模块管理器。然后Node.js保存它们供以后使用。//保存函数执行的返回结果Localloader_exports;if(!ExecuteBootstrapper(this,"internal/bootstrap/loaders",&loaders_params,&loaders_args).ToLocal(&loader_exports)){returnMaybeLocal();}Localloader_exports_obj=loader_exports.As();//获取C++模块加载器Localinternal_binding_loader=loader_exports_obj->Get(context(),internal_binding_string()).ToLocalChecked();//保存C++模块加载Set_internal_binding_loader(internal_binding_loader.As());//获取原生JS加载器Localrequire=loader_exports_obj->Get(context(),require_string()).ToLocalChecked();//保存原生JS加载器Set_native_module_require(require.As<函数>());1.3执行用户JSNode.js初始化后最终会通过下面的代码执行用户的代码。StartExecution(env,"internal/main/run_main_module")查看StartExecution。MaybeLocalStartExecution(Environment*env,constchar*main_script_id){EscapableHandleScopescope(env->isolate());CHECK_NOT_NULL(main_script_id);std::vector>parameters={env->process_string(),//require数env->require_string(),env->internal_binding_string(),env->primordials_string(),FIXED_ONE_BYTE_STRING(env->isolate(),"markBootstrapComplete")};std::vector>arguments={env->process_object(),//原生JS和C++模块加载器env->native_module_require(),env->internal_binding_loader(),env->primordials(),env->NewFunctionTemplate(MarkBootstrapComplete)->GetFunction(env->context()).ToLocalChecked()};returnscope.EscapeMaybe(ExecuteBootstrapper(env,main_script_id,¶meters,&arguments));}传入了两个加载器,然后执行run_main_module.js。核心代码如下require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);Module.runMain的代码如下functionexecuteUserEntryPoint(main=process.argv[1])){Module._load(main,null,true);}最后通过_load完成用户代码的加载和执行,下面我们将详细分析各种加载器。2模块加载的实现我们通常通过require加载模块,require为我们搞定一切。其实Node.js中有很多类型的模块,下面我们会一一介绍。2.1JSON模块Module._extensions['.json']=function(module,filename){constcontent=fs.readFileSync(filename,'utf8');try{module.exports=JSONParse(stripBOM(content));}catch(错误){err.message=filename+':'+err.message;throwerr;}};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模块加载器,所以require只能在原生JS模块中加载原生JS模块。另外,还有一个实参需要注意,那就是internalBinding,internalBinding是用来加载C++模块的,所以在原生JS中,可以通过internalBinding来加载C++模块。2.4C++Module2.5AddonModule后记:模块机制在任何语言中都是非常基础和重要的部分。如果你对Node.js模块机制的原理有深刻的理解,我们知道当你需要时会发生什么。如果对具体实现感兴趣,可以阅读Node.js的源码,或者看看仓库https://github.com/theanarkh/js_runtime_loader。