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

Module(一)-Module

时间:2023-04-03 11:00:22 Node.js

Node.js模块系统介绍Node.js有一个简单的模块加载系统。在Node.js中,文件和模块之间存在一一对应关系(每个文件都被视为一个单独的模块)。例如,考虑以下名为foo.js的文件:constcircle=require('./circle.js');console.log(`半径为4的圆的面积为${circle.area(4)}`);在第一行,foo.js将模块circle.js加载到与foo.js相同的目录中。circle.js的内容如下:constPI=Math.PI;exports.area=(r)=>PI*r*r;exports.circumference=(r)=>2*PI*r;模块circle.js对外分别暴露了函数area()和circumference()。所以如果你想对外暴露函数或对象,你可以将它们赋值给一个特殊的exports对象。使用exports和module.exports的区别varmodule={exports:{}};//1:在exports中添加一个字段,并为该字段赋值(function(module,exports){exports.a=1;})(模块,模块.exports);console.log(module.exports.a);//=>1console.log(JSON.stringify(module));//=>{"exports":{"a":1}}//2:覆盖并赋值exports(function(module,exports){exports={b:1};})(module,module.exports);console.log(module.exports.b);//=>undefinedconsole.log(JSON.stringify(module));//=>{"exports":{"a":1}}//3:分别给module.exports和exports添加字段,并为字段赋值(function(module,exports){module.exports.c='第一个C';exports.c='第二个C';})(module,module.exports);console.log(module.exports.c);//=>第二个Cconsole.log(JSON.stringify(module));//=>{"exports":{"a":1,"c":"secondC"}}//4:给module.exports赋值,exports添加字段并赋值(function(module,exports){module.exports={d:'firstD'};exports.d='secondD';})(module,module.exports);console.log(module.exports.d);//=>首先Dconsole.log(JSON.stringify(module));//=>{"exports":{"d":"firstD"}}内部变量模块内部的变量最终必须是私有的,因为模块的内容将被Node.js包装在一个函数中(见下面的模块包装器))上面的例子中,变量PI是circle.js的私有变量,无法从外部获取。如果你想让模块暴露一个函数(比如构造函数),或者一个完整的对象。那么你需要将它分配给module.exports而不是exports。(原因请参考上面的示例代码)下面的bar.js中,使用了square模块,导出了一个函数:constsquare=require('./square.js');varmySquare=square(2);console.log(`我的方块的面积是${mySquare.area()}`);在square.js模块中定义一个square方法:module.exports=(width)=>{return{area:()=>width*width;};}此外,模块系统在require("module")模块中实现。“main”模块当一个模块直接从Node.js运行时,它会为该模块设置require.main。您可以使用它来测试模块是直接运行还是需要运行。require.main===module以文件foo.js为例,如果你运行nodefoo.js这个属性为真。运行require('./foo.js')是错误的。因为module提供了一个文件名(通常等同于__filename),通过查看require.main.filename就可以得到当前应用程序的入口点。包管理器的一些技巧Node.js的require()函数支持一些合理的目录结构。它允许包管理器程序(例如dpkg、rpm和npm)直接从Node.js模块构建本机包而无需修改。下面我们给出了一个可行的建议目录结构:假设我们希望/usr/lib/node//中的文件夹指定包的版本。此外,包可以相互依赖。比如你要安装foo包,而这个包可能需要安装指定版本的bar包。bar包也有可能依赖其他包,在某些特殊情况下,这些依赖的包甚至会产生循环依赖。由于Node.js会寻找所有加载模块的真实路径(即解析软链接),然后去node_modules文件夹中寻找依赖包,下面的解决方案可以很简单的解决这个问题:/usr/lib/node/foo/1.2.3/-包含foo包,版本1.2.3/usr/lib/node/bar/4.3.2/-包含foo依赖的bar包/usr/lib/node/foo/1.2.3/node_modules/bar-软链接到/usr/lib/node/bar/4.3.2//usr/lib/node/bar/4.3.2/node_modules/*-软链接到bar的依赖项,所以即使循环遇到依赖,或者依赖冲突,每个模块都可以加载到它所依赖的指定版本的包中使用。foo包中require('bar')时,可以软链接到指定版本的/usr/lib/node/foo/1.2.3/node_modules/bar。然后,bar包中的代码调用require('quux')时,也可以软链接到指定版本的/usr/lib/node/bar/4.3.2/node_modules/quux。模块加载的全过程(重要,下面写的伪代码过程一定要记住)要得到调用require()时要加载的确切文件名,请使用require.resolve()函数。下面是模块加载的全过程和require.resolve的解析过程://loadXmodulerequire(X)frommoduleatpathy1.如果X是核心模块。A。返回核心模块b。停止2。如果X以'./'或'/'或'../'开头LOAD_AS_FILE(Y+X)b.LOAD_AS_DIRECTORY(Y+X)3。LOAD_NODE_MODULES(X,目录名(Y))4。THROW"notfound"//加载X文件//加载过程:X->X.js->X.json->X.nodeLOAD_AS_FILE(X)1.如果[X]是文件,则将[X]作为JavaScript文本加载。停止2。如果[X.js]是一个文件,将[X.js]作为JavaScript文本加载。STOP3。如果[X.json]是一个文件,将[X.json]作为JavaScript文本加载。第四步。如果[X.node]是一个文件,将[X.node]作为JavaScript文本加载。STOP//加载入口文件//加载过程:X->X/index.js->X/index.json->X/index.nodeLOAD_INDEX(X)1。如果[X/index.js]是一个文件,将[X/index.js]作为JavaScript文本加载。停止2。如果[X/index.json]是一个文件,将[X/index.json]作为JavaScript文本加载。STOP3。如果[X/index.node]是文件,则将[X/index.node]作为JavaScript文本加载。STOP//加载文件夹LOAD_AS_DIRECTORY(X)1。如果[X/package.json]我是一个文件。A。解析[X/package.json],寻找“main”字段b。让M=X+(json主字段)c。LOAD_AS_FILE(M)d.LOAD_INDEX(M)2。LOAD_INDEX(X)//加载节点模块LOAD_NODE_MODULES(X,START)1.让DIRS=NODE_MODULES_PATHS(START)2。对于DIRS中的每个DIR;A。LOAD_AS_FILE(DIR/X)b.LOAD_AS_DIRECTORY(DIR/X)//列出所有可能的node_modules路径NODE_MODULES_PATHS(START)1.letPARTS=pathsplit(START);2.令I=零件数-13.令DIRS=[]4.当I>0a.如果PARTS[I]="node_modules"CONTINUEb.DIR=pathjoin(PARTS[0...I]+"node_modules")c.DIRS=DIRS+DIRd.让我=我-15。returnDIRSmodulecache所有的模块都会在第一次加载后被缓存,这意味着你每次调用require('foo')时都会得到完全相同的对象。多次调用require('foo')可能不会多次执行模块的代码。这是一个重要的功能。使用它,可以返回“部分完成”的对象,允许模块根据依赖关系逐层加载,即使这样做可能会导致循环依赖。如果你想让一个模块在每次加载时都执行代码,你需要导出一个函数并调用该函数。模块缓存注意事项模块根据其解析的文件名进行缓存。根据调用模块的路径,被调用模块可能解析为不同的文件名(从node_modules文件夹加载)。它不保证require('foo')每次解析出不同的文件时总是返回相同的对象。此外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向同一个文件,但缓存仍会将它们视为不同的模块,并将多次重新加载文件。例如,require('./foo')和require('./FOO')返回两个不同的对象,无论./foo和./FOO是否是同一个文件。核心模块Node.js有一些模块被编译成二进制文件。这些模块在本文档的其他地方有更详细的描述。核心模块位于Node.js源代码的lib/文件夹中。如果将核心模块的模块ID传递给require(),则它们总是首先加载。比如,即使有一个自定义的模块叫http,我们执行require('http')也会一直返回内置的HTTP模块,循环引用当循环引用require()时,返回的模块可能没有完成。考虑这种情况:a.js:console.log('astarting');exports.done=false;constb=require('./b.js');console.log('ina,b.done=%j',b.done);exports.done=true;console.log('adone');b.js:console.log('bstarting');exports.done=false;consta=require('./a.js');console.log('inb,a.done=%j',a.done);exports.done=true;控制台。log('bdone');app.js:console.log('mainstarting');consta=require('./a.js');constb=require('./b.js');console.log('inmain,a.done=%j,b.done=%j',a.done,b.done);当app.js加载a.js时,a.js依次加载b.js。这时,b.js尝试加载a。js。为防止无限循环,将a.js导出对象的未完成副本返回给b.js模块。b.js然后完成加载,并将其导出的对象提供给a.js模块。到app.js加载这两个模块时,它们都已完成。所以这个程序的输出是:$nodeapp.jsmainstartingastartingbstartinginb,a.done=falsebdoneina,b.done=trueinmain,a.done=true,b.done=true模块包装器正在执行在模块代码之前,Node.js将使用函数包装器来包装模块内容,如下所示:(function(exports,require,module,__filename,__dirname){//yourmodulecode});通过执行此操作,Node.js实现了以下内容:它将模块内的顶级变量(定义为var、const或let)限定在模块内而不是全局范围内。它有助于在模块内部提供一些实际上只属于模块的全局变量,例如:module和exports对象用于帮助从模块内部导出一些值变量__filename和__dirname是文件名和文件最终由当前模块文件夹路径模块对象签名Objectmodule{id:String,//模块标识,为模块文件在系统中的绝对路径|undefined,//引用模块Parentmodulefilename:String|null,//最终解析的文件名,同__filename。loaded:Boolean,//模块是否已经加载children:Array,//更改模块路径的引用列表:Array//模块加载路径}requirefunctionsignatureFunctionrequire{[Function],//functionbodyresolve:Function,//根据模块ID解析模块,返回绝对路径main:undefined|Object,//应用的主要(main)模块extensions:{'.js':Function,'.json':Function,'.node':Function},cache:Object//模块缓存,带绝对路径的模块作为关键}