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

【模块化系列】Nodejs模块化原理

时间:2023-04-03 13:07:14 Node.js

一、前言node的应用是由模块组成的。Node遵循commonjs模块规范,隔离每个模块的作用域,让每个模块都在自己的命名空间中。在实施中。commonjs的主要内容:模块必须通过module.exports导出外部变量或接口,通过require()将其他模块的输出导入到当前模块作用域中。Commonjs模块特点:1.所有代码运行在当前模块作用域内,不会污染全局作用域。2.模块同步加载,按照代码顺序加载。3.模块可以多次加载,第一次加载时只运行一次,然后缓存运行结果,后面加载时直接从缓存中读取结果.如果你想让模块再次运行,你必须清除缓存。我们看一个简单的例子,写一个demo-exports.jsletname='saucxs';letgetName=function(name){console.log(name)};module.exports={name:name,getName:getName让我们再写一个demo-require.jsletperson=require('./demo-export')console.log(person,'------------')//{name:'saucxs',getName:[Function:getName]}console.log(person.name,'===========')//saucxsperson.getName('gmw');//gmwperson.name='updateName'console.log(person,'22222222')//{name:'updateName',getName:[Function:getName]}console.log(person.name,'3333333')//updateNameperson.getName('gmw')//gmwnodedemp-require.js,结果如上所示。2.模块对象commonjs规范,每个文件都是一个模块,每个模块都有一个模块对象,指向当前模块。模块对象具有以下属性:(1)id:当前模块id。(2)exports:表示当前模块对外暴露的值。(3)parent:是一个对象,表示调用当前模块的模块。(4)children:是一个对象,表示当前模块调用的模块。(5)filename:模块的绝对路径(6)paths:从当前模块开始搜索node_modules目录,然后依次进入父目录,在父目录下搜索node_modules目录;依次迭代,直到根目录下的node_modules目录。(7)loaded:布尔值,表示当前模块是否加载完毕。下面举个栗子module.jsmodule.exports={name:'saucxs',getName:function(name){console.log(name)}}console.log(module)nodemodule.js1,module.exports我们知道模块对象有一个exports属性,用来暴露变量、方法或者整个模块。当其他文件需要require模块时,实际读取的是模块对象中的exports属性。2、既然exports对象有module.exports可以满足所有需求,那为什么还要exports对象呢?我们来看看两者的关系:(1)exports对象和module.exports都是引用类型变量,指向同一个内存地址。在node中,两者都指向开头的一个空对象。exports=module.exports={}(2)其次,exports对象作为形参传入,直接赋值给形参的引用,但作用域外的值不能改变。var模块={出口:{}};varexports=module.exports;functionchange(exports){/*在形参exports中添加属性name,会同步到外部module.exports对象*/exports.name='saucxs'/*此处修改wxports的引用不会影响module。exports*/exports={age:18}console.log(exports)//{age:18}}change(exports);console.log(module.exports);//{exports:{name:'saucxs'}}分析上面的代码:直接给exports赋值会改变当前模块内部形参exports的对象应用。也就是说当前的exports对象与外部的module.exports对象没有关系,所以改变exports对象不会影响module.exports。注意:创建module.exports是为了解决上面直接赋值exports会导致抛出不成功的问题。//这些操作是合法的exports.name='saucxs';exports.getName=function(){console.log('saucxs')};//相当于下面的方法module.exports={name:'saucxs',getName:function(){console.log('saucxs')}}//或者更常规的写法letname='saucxs';letgetName=function(){console.log('saucxs')}module.exports={name:name,getName:getName}这样就不用每次都直接把要抛出的对象或方法赋值给exports属性,直接使用对象字面量更方便。3、require方法require是模块的导入规则。通过exports或者module.exports抛出一个模块,通过require方法传入模块标识。然后node按照一定的规则导入模块,我们就可以使用模块中定义的方法了。和属性。(一)node引入模块的机制1.node引入模块需要经过三个步骤:(1)路径分析(2)文件定位(3)编译执行2.在node中,模块被划分分为两种:(1)node提供的模块,如http模块、fs模块等,称为核心模块。核心模块在节点源代码编译过程中编译了二进制文件。node进程启动时,部分核心模块是直接加载到内存中的,所以这些模块不需要经过上面的(2)、(3)步骤,在路径分析时先判断,所以加载速度是最快的。(2)用户自己编写的模块称为文件模块。文件模块需要按需加载,需要经过以上三个步骤,速度比较慢。3.优先从缓存中加载正如浏览器缓存静态脚本文件以提高页面性能一样,Node也会缓存导入的模块。与浏览器的不同之处在于,Node缓存编译和执行的对象,而不是静态文件。让我们看一下requireA.jsconsole.log('modulerequireAstartsloading...')exports=function(){console.log('Hi')}console.log('modulerequireAisloaded')init.jsvarmod1=require('./requireA')varmod2=require('./requireA')console.log(mod1===mod2)执行nodeinit.js虽然我们引入了两次requireA模块,但是在模块中代码实际上只执行一次。而mod1和mod2指向同一个模块。4.module._load源码Module._load=function(request,parent,isMain){//计算绝对路径varfilename=Module._resolveFilename(request,parent);//第一步:如果有缓存,取出缓存varcachedModule=Module._cache[filename];如果(cachedModule){返回cachedModule.exports;//第二步:是否是内置模块if(NativeModule.exists(filename)){returnNativeModule.require(filename);}//No.第3步:生成模块实例并将其存储在缓存中varmodule=newModule(filename,parent);Module._cache[文件名]=模块;//第四步:加载模块try{module.load(filename);hadException=false;}finally{if(hadException){deleteModule._cache[文件名];}}//第五步:输出模块的exports属性returnmodule.exports;};对应的流程如下:(二)路径分析及文件定位1.路径分析模块标识符分析:(1)核心模块,如http、fs模块。(2).开头的相对路径文件模块。或者../。(3)以/开头的绝对路径模块。(4)非路径形式的文件模块。分析:(1)核心模块:优先级仅次于缓存,加载速度最快。如果自定义模块与核心模块同名,加载将失败。如果要成功,必须修改自定义模块的名称或更改路径。(2)路径形式的文件模块:以.开头的标识符。or..or/将被视为文件模块。在加载过程中,require方法将路径转换为真实路径,加载速度仅次于核心模块。(3)非路径形式的自定义模块:这是一种特殊的文件模块,可以是文件形式,也可以是包形式。查找此类模块的策略类似于js作用域链。Node会一一尝试模块路径中的路径,直到找到目标文件。注:这是node定位文件模块具体文件时的查找策略,具体表现为一个路径数组。可以在REPL环境中输出Module对象,查看其path属性即可查看上述数组。文章开头的paths数组:2.文件位置(1)文件扩展名分析require()分析出的标识符可能不包含扩展名,node扩展名会按照.js,.node,和.json,依次尝试(2)目标解析和打包。如果在扩展分析的步骤中,找到了对应的目录而不是文件,node会将该目录当做一个包,下一步的分析就是在package.json中找到main属性指定的文件名当前目录。如果搜索失败,依次搜索index.js、index.node、index.json。如果在目录解析中没有找到文件,自定义模块会进入下一个模块路径继续查找,直到遍历完所有模块路径,如果仍然没有找到,则抛出查找失败异常。(3)参考源代码。Module._findPath方法在Module._load方法内部被调用。该方法用于返回模块的绝对路径。源码如下:Module._findPath=function(request,paths){//列出所有可能的扩展名:.js,.json,.nodevarexts=Object.keys(Module._extensions);//如果是绝对路径,不再搜索if(request.charAt(0)==='/'){paths=[''];}//目录是否有后缀斜杠vartrailingSlash=(request.slice(-1)==='/');//第一步:如果当前路径已经在缓存中,直接返回缓存varcacheKey=JSON.stringify({request:request,paths:paths});如果(Module._pathCache[cacheKey]){返回Module._pathCache[cacheKey];}//Step2:依次遍历所有路径for(vari=0,PL=paths.length;i