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

如何在Node中引入一个模块及其细节

时间:2023-03-15 10:17:41 科技观察

在node环境中,有两个内置的全局变量可以不用导入直接使用,而且无处不在。它们构成了nodejs的模块系统:module和require。下面是一个简单的例子constfs=require('fs')constadd=(x,y)=>x+ymodule.exports=add虽然他们只是在正常使用中导入和导出模块,但是稍微深入一点,你可以看到世界那么大。它们可以在行业中用来做一些棘手的事情。虽然我不推荐使用这些黑科技,但是了解一点还是很有必要的。如何在不重启应用程序的情况下热加载模块?比如require一个json文件的时候,会产生缓存,但是重写文件的时候怎么看呢?如何在不侵入代码的情况下打印日志?循环引用会造成什么问题?modulewrapper当我们用node写一个模块的时候,模块其实是被一个函数包装起来的,如下图:(function(exports,require,module,__filename,__dirname){//所有模块代码都包装在这个函数中constfs=require('fs')constadd=(x,y)=>x+ymodule.exports=add});因此,在一个模块中会自动注入以下变量:exportsrequiremodule__filename__dirnamemodule最好的调试方式就是打印,我们想知道模块在哪里,那就打印出来吧!constfs=require('fs')constadd=(x,y)=>x+ymodule.exports=addconsole.log(module)module.id:如果是.,表示入口模块,否则为文件名的模块,可以看到下面的koamodule.exports:[1]?从下面的源码可以看出模块包装器module._compile的调用者是如何注入内置变量的,所以根据源码很容易理解一个模块中的变量:exports:实际上是引用module.exportsrequire:大多数情况下是Module.prototype.requiremodule__filename__dirname:path.dirname(__filename)///internal/modules/cjs/loader.js:1138Module.prototype._compile=function(content,filename){//...constdirname=path.dirname(filename);constrequire=makeRequireFunction(this,redirects);letresult;//从这里可以看出:exports=module.exportsconstexports=this.exports;constthisValue=exports;constmodule=this;if(requireDepth===0)statCache=newMap();if(inspectorWrapper){result=inspectorWrapper(compiledWrapper,thisValue,exports,require,module,filename,dirname);}else{result=compiledWrapper.call(thisValue,exports,require,module,filename,dirname);}//...}require通过nodeREPL控制台调试,或者在VSCode中输出require,你会发现require是一个极其复杂的从上述模块包装器的源码中也可以看出require对象是通过makeRequireFunction函数生成的,如下///internal/modules/cjs/helpers.js:33functionmakeRequireFunction(mod,redirects){constModule=mod.constructor;letrequire;if(redirects){//...}else{//require其实就是Module.prototype.requirerequire=functionrequire(path){returnmod.require(path);};}functionresolve(request,options){//...}require.resolve=resolve;函数路径(request){validateString(request,'request');returnModule._resolveLookupPaths(request,mod);}resolve.paths=paths;require.main=process.mainModule;//Enablesupporttoaddextraextensiontypes.require.extensions=Module._extensions;require.cache=Module._cache;returnrequire;}?更详细的require可以参考官方文档:NodeAPI:require[2]?require(id)require函数用于导入一个模块,它也是最常用的///internal/modules/cjs/loader.js:1019Module.prototype.require=function(id){validateString(id,'id');if(id===''){thrownewERR_INVALID_ARG_VALUE('id',id,'mustbeanon-emptystring');}requireDepth++;try{returnModule._load(id,this,/*isMain*/false);}finally{requireDepth--;}}当require引入一个模块,其实是通过Module._load加载的,大体总结如下:如果Module._cache命中模块缓存,直接取出module.exports,如果加载后是NativeModule,则loadNativeModule加载模块这样asfs,http,path等,并加载End否则,使用Module.load加载模块。当然这一步也很长,下一章会详细讲解///internal/modules/cjs/loader.js:879Module._load=function(request,parent,isMain){letrelResolveCacheIdentifier;if(parent){//...}constfilename=Module._resolveFilename(request,parent,isMain);constcachedModule=Module._cache[filename];//如果命中缓存,直接拿去Cacheif(cachedModule!==undefined){updateChildren(parent,cachedModule,true);returncachedModule.exports;}//如果是NativeModule就加载它constmod=loadNativeModule(filename,request);if(mod&&mod.canBeRequiredByUsers)returnmod.exports;//不要调用updateChildren(),Moduleconstructoralreadydoes.constmodule=newModule(filename,parent);if(isMain){process.mainModule=module;module.id='.';}Module._cache[filename]=module;if(parent!==undefined){//...}letthrew=true;try{if(enableSourceMaps){try{//如果不是NativeModule,就加载module.load(filename);}catch(err){rekeySourceMap(Module._cache[filename],err);throwerr;/*node-do-not-add-exception-line*/}}else{module.load(filename);}threw=false;}finally{//...}returnmodule.exports;};require.cache"代码执行require(lib)时,内容lib模块中的内容会被执行并作为缓存,下次引用时不会执行模块中的内容”这里的缓存指的是require.cache,也就是上一段提到的Module._cache///internal/modules/cjs/loader.js:899require.cache=Module._cache;这是一个小测试:?有两个文件:index.js和utils.js。utils.js中有打印操作。当index.js多次引用utils.js时,utils.js中的打印操作会执行多次。代码示例如下?"index.js"//index.js//引用了两次require('./utils')require('./utils')"utils.js"//utils.jsconsole.log('isexecutedOnce')"答案只执行一次",所以require.cache,在index.js最后打印require,会发现一个模块缓存//index.jsrequire('./utils')require('./utils')console.log(require)那么回到本章开头的问题:?如何不重启应用热加载模块??回答:“DeleteModule._cache”,但是同时会出问题,比如这一行deleterequire.cache[3]导致的该死的内存泄漏所以,这种大幅修改核心代码的黑魔法开发环境可以玩,生产环境就别去了,毕竟黑魔法是不可控的。总结在模块中执行时,会被modulewrapper包裹,注入到全局变量require和module中。module.exports和exports的关系其实是exports=module.exportsrequire其实是module.requirerequire.cache会保证模块不会被多次执行,不要使用删除require.cache的黑魔法