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

Node模块加载层级优化

时间:2023-04-03 15:13:13 Node.js

模块加载痛点大家对node模块加载机制也有或多或少的了解。最粗浅的表现就是从当前目录向上一层一层查询node_modules目录,找到依赖就加载。但是随着应用规模的增大,目录层次越来越深。如果要在一个模块中使用require,依赖名称或者相对路径来引用其他模块,会很麻烦,影响开发效率和美观。示例演示://当前目录:/usr/local/test/index.js//gulp模块的路径为/usr/lib/node_modulesvargulp=require('../../lib/gulp');吞咽。task('say',function(){console.log('helloworrd');});目前条件下,只能使用上面提到的相对路径来引用依赖模块。我们可以看到上面引用的缺点:丑陋、非常复杂、容易出错、难以维护。第二个缺点是最不能接受的。当模块被多次引用时,问题会被放大。因此,迫切需要找到解决多层目录依赖引用的方案。本文将讨论作者的发展。过程中的一些尝试,欢迎讨论其他可行方案。由于全局变量法的目标是解决相对目录层次结构不美观且难以理解,所以可以尝试用变量来代替目录层次结构。这种方案最直接,node加载依赖最快,不需要遍历其他层级目录。但是为了更通用,作者经常使用全局变量来绑定目录关系:demo://当前目录:/usr/local/test/index.js//gulp模块所在路径为/usr/lib/node_modulesglobal._root='/usr/lib/node_modules';varpath=require('path');vargulp=require(path.join(_root,'gulp'));...这个方案最直接,但是扩展性不强,尤其是多人维护的情况下,所以建议在单人开发的小项目中使用。Directoryreferencethemodulename直接引用模块名,说到底就是直接引用node_modules目录下的依赖,类似于引用node默认加载的那些模块,比如http、event模块。demo://当前目录:/usr/local/test/index.js//gulp模块的路径为/usr/lib/node_modulesvargulp=require('gulp');...在/usr目录下/local/test,/usr/local,/usr,/四个目录下没有“node_modules”目录或者“node_modules”目录下没有gulp模块,那么运行这个文件肯定会报错“MODULE_NOT_FOUND”,这就是我们接下来要解决的问题,就是如何修改节点加载依赖的层级关系。修改依赖加载级别相信大家在学习node的时候都看过一本书《深入浅出nodejs》。本书第二章简单介绍了节点加载依赖遍历的一些目录。结果中的输出module.paths是一个数组,如['/usr/local/test/node_modules','/usr/local/node_modules','/usr/node_modules','/node_modules']这给了我们一个启发,即加载某个模块的顺序是根据上述数组项的先后顺序判断该模块是否存在,存在则加载。其实node确实是这么做的(猜测的正确性会在下面的源码中分析)。那么,在猜想的基础上,我们可以尝试修改数组,看是否能影响这个模块加载依赖的顺序。如果成功了,那就美了,如果失败了,我们需要找到更合适的解决方案。尝试1://当前目录:/usr/local/test/index.js//gulp模块的路径为/usr/lib/node_modulesmodule.paths.push('/usr/lib/node_modules');console。日志(模块。路径);vargulp=require('gulp');执行命令,一切正常,成功。从输出信息可以看出['/usr/local/test/node_modules','/usr/local/node_modules','/usr/node_modules','/node_modules','/usr/lib/node_modules']didmodify增加了依赖搜索层级,但是可以看出设置的目录是数组的最后一个,也就是说node在找到gulp依赖之前会遍历4层目录,最后在第5层找到级目录。如果项目中只引用gulp还好,但是随着其他依赖的增多,运行时加载/usr/lib/node_modules下的依赖会耗费大量时间。因此,建议大家评估项目中依赖的位置,合适的话可以优先加载手动设置的依赖目录://当前目录:/usr/local/test/index.js//所在路径gulp模块位于/usr/lib/node_modulesmodule.paths.unshift('/usr/lib/node_modules');console.log(module.paths);vargulp=require('gulp');这样,我们就不知道底层节点是如何工作的目标实现了。哈哈,不过作为靠谱的前端(节点)工程师,我们不会满足于这个水平吧?哈哈!深入源码探索作者提取了模块(依赖)加载相关的代码://初始化全局依赖加载路径Module._initPaths=function(){...varpaths=[path.resolve(process.execPath,'..','..','lib','node')];如果(homeDir){paths.unshift(path.resolve(homeDir,'.node_libraries'));paths.unshift(path.resolve(homeDir,'.node_modules'));}//这里需要重点获取环境变量“NODE_PATH”varnodePath=process.env['NODE_PATH'];如果(nodePath){paths=nodePath.split(path.delimiter)。连接(路径);}//modulePaths记录了全局加载依赖的根目录,在Module._resolveLookupPaths中用到modulePaths=paths;//克隆为只读副本,用于自省。Module.globalPaths=modulePaths.slice(0);};//@params:request为加载的模块名//@params:parent为当前模块(即加载依赖的模块)Module._resolveLookupPaths=function(请求,父级){...varstart=request.substring(0,2);//如果是引用模块名的方式,那就是require('gulp')if(start!=='./'&&start!=='..'){//这里的modulePaths就是Module._initPaths函数中分配的变量var路径s=模块路径;如果(父级){如果(!parent.paths)parent.paths=[];paths=parent.paths.concat(路径);}返回[请求,路径];}//使用eval执行可执行文件在字符串的情况下,parent.id和parent.filename为空if(!parent||!parent.id||!parent.filename){varmainPaths=['.'].concat(模块路径);mainPaths=Module._nodeModulePaths('.').concat(mainPaths);返回[请求,主路径];}...};Module._initPaths函数在默认生命周期内只执行一次,其作用自然是设置全局加载依赖的相对路径,而每次在文件中执行require加载其他依赖时,Module._resolveLookupPaths函数都会被执行,返回一个包含依赖名和可遍历目录的数组(这个数组中的目录项可以加载到依赖中,也可以不加载)。最后的工作就是根据Module._resolveLookupPaths函数返回的结果遍历目录数组,加载依赖。如果遍历后没有找到依赖项,则抛出错误。分析完源码,相信大家已经注意到了一些信息:Module._initPaths函数内部会检查NODE_PATH环境变量。Module._initPaths函数只执行一次。Module._initPaths函数初始化的全局依赖加载路径与module.paths相关,我们可以换个角度解决依赖加载的问题。环境变量法通过上一节的源码分析,我们知道了NODE_PATH的作用,那么如何使用或者优雅地使用NODE_PATH来解决依赖加载问题呢?最直接的尝试方法是修改系统的环境变量。linux下执行exportNODE_PATH=/usr/lib/node_modules即可解决。不过这个方案毕竟不优雅,因为我们的一个项目修改了系统的环境变量。如果其他项目也采用这种方案,那么相信系统的NODE_PATH会变得很长,而且会因为NODE_PATH的子路径顺序问题而产生意外冲突,所以不推荐这种方案。尝试2我们希望只为当前运行的程序设置环境变量,而不影响其他程序;而一旦当前程序退出,设置的环境变量也会恢复。满足这个需求最直观的方式就是命令行配置。查阅node手册,可以这样运行:NODE_PATH=/usr/lib/node_modulesnode/usr/local/test/index.js这样gulp依赖依然可以成功加载,不会影响系统的环境变量。但是,命令行的方式显而易见,丑陋且麻烦。每次运行程序,都需要提前输入一系列路径。这种方式把代码的可维护性变成了程序的可维护性,不适合在负责任的项目中使用。在尝试跑三个节点的时候,它给我们提供了一个变量,没错,就是进程。process是node默认加载的Process模块??的一个属性,通过它可以获取应用进程的相关信息,包括设置的环境变量。我们可以在应用的入口文件中设置环境变量://当前目录:/usr/local/test/index.js//gulp模块所在路径为/usr/lib/node_modulesprocess.env.NODE_PATH='/usr/lib/node_modules';vargulp=require('gulp');这样,当我们执行文件的时候,出现了意想不到的情况,仍然报“MODULE_NOT_FOUND”错误。为什么是这样?原因还是要追溯源码。源码分析部分总结了三点,第二点提到Module._initPaths函数只执行一次,也就是说当我们在代码中设置process.env.NODE_PATH='/usr/lib/node_modules';,但是,由于此时已经执行了Module._initPaths,所以没有使用设置的环境变量。解决这个问题也比较简单,就是再次调用Module._initPaths。//当前目录:/usr/local/test/index.js//gulp模块所在路径为/usr/lib/node_modulesprocess.env.NODE_PATH='/usr/lib/node_modules';require('module').Module._initPaths();//或module.constructor._initPaths()vargulp=require('gulp');这样就安全无害地解决了多基目录下的依赖调用问题。总结本文从实际开发中遇到的问题出发,提出解决多基目录依赖的几种方案:全局变量法修改module.paths方法环境变量法(三种实现)当然社区还是有一些帮助的解决这类问题的模块,比如“app-module-path”,但是思路是差不多的。在这里把自己的学习收获分享给大家,希望能给大家一些启发和感悟,非常感谢!