当前位置: 首页 > 后端技术 > Node.js

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

时间:2023-04-03 17:29:14 Node.js

本文收录于GitHub山月行博客:shfshanyue/blog,里面包含了我在实际工作中遇到的问题,思考业务和学习前端的方向全栈工程系列Node进阶系列在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:如果是.,表示入口模块,否则就是module的文件名,我们可以看到如下koamodule.exports:从下面的源码可以看出模块包装器的调用者module._compile是如何注入内置变量的,所以根据源码很容易理解一个模块中的变量:exports:其实就是一个引用到module.exports要求:在大多数情况下Module.prototype.requiremodule__filename__dirname:path.dirname(__filename)///internal/modules/cjs/loader.js:1138Module.prototype._compile=function(content,filename){//...常量目录e=path.dirname(文件名);constrequire=makeRequireFunction(this,redirects);让结果;//由此可见:exports=module.exportsconstexports=this.exports;constthisValue=exports;const模块=this;如果(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,从上面的modulewrapper可以发现require是一个极其复杂的对象从源码中也可以看出require是由makeRequireFunction函数,如下///internal/modules/cjs/helpers.js:33functionmakeRequireFunction(mod,redirects){constModule=mod.constructor;让要求;if(redirects){//...}else{//require实际上是Module.prototype.requirerequire=functi关于要求(路径){返回mod.require(路径);};}functionresolve(request,options){//...}require.resolve=resolve;函数路径(请求){validateString(请求,'请求');返回Module._resolveLookupPaths(请求,mod);}resolve.paths=路径;require.main=process.mainModule;//启用支持以添加额外的扩展类型。require.extensions=Module._extensions;require.cache=模块._cache;returnrequire;}更详细的require可以参考官方文档:NodeAPI:requirerequire(id)require函数用于导入一个模块,也是最常用的函数///内部/模块/cjs/loader.js:1019Module.prototype.require=function(id){validateString(id,'id');if(id===''){thrownewERR_INVALID_ARG_VALUE('id',id,'必须是非空字符串');}requireDepth++;try{returnModule._load(id,this,/*isMain*/false);}最后{requireDepth--;}}require引入模块时,实际通过Module._load加载,大体总结如下:如果Module._cache命中模块缓存,则直接取出module.exports,如果加载的是NativeModule,则loadNativeModule加载模块,如fs、http、path等模块,否则,使用Module.load加载模块,当然这一步也很长,下一章会详细讲解///internal/modules/cjs/loader.js:879Module._load=function(request,parent,isMain){让relResolveCacheIdentifier;if(parent){//...}constfilename=Module._resolveFilename(request,parent,isMain);constcachedModule=Module._cache[文件名];//如果命中缓存,直接获取缓存if(cachedModule!==undefined){updateChildren(parent,cachedModule,true);返回cachedModule.exports;}//如果是NativeModule,就加载它constmod=loadNativeModule(filename,request);如果(mod&&mod.canBeRequiredByUsers)返回mod.exports;//不要调用updateChildren(),模块构造函数已经调用了。constmodule=newModule(文件名,parent);如果(isMain){process.mainModule=module;module.id='.';}Module._cache[文件名]=模块;如果(父母!==undefined){//...}让thrown=true;try{if(enableSourceMaps){try{//如果不是NativeModule,加载它module.load(filename);}catch(err){rekeySourceMap(Module._cache[文件名],err);抛出错误;/*node-do-not-add-exception-line*/}}else{module.load(filename);}抛=假;}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('wasexecutedonce')答案只执行一次,所以require.cache,在index.js末尾打印require,此时会找到模块缓存//index.jsrequire('./utils')require('./utils')console.log(require)让我们回到本章开头的问题:如何在不重启应用的情况下热加载模块?答:删除Module._cache,但同时会出现问题,比如一行deleterequire.cache导致的内存泄漏血案。所以,这种大幅度修改核心代码的黑魔法,在开发环境中是可以发挥的。生产环境就不要去了,毕竟黑魔法是不可控的。综上所述,当模块执行时,会被模块包装器包裹起来,注入到全局变量require和module中。module.exports和exports的关系其实是exports=module.exportsrequire其实就是module.requirerequire.cache会保证module不会执行太多这个时候不要用deleterequire.cache的黑魔法,跟着我前端工程系列Node进阶系列欢迎关注公众号【全栈成长之路】,定期推送Node原创和全栈成长文章<图>

欢迎关注全栈成长之路