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

NodeJS中的模块是单例吗?

时间:2023-03-21 13:51:29 科技观察

本文由Lazlojuly翻译自are-node-js-modules-singletons。本文属于作者***实践中的NodeJS入门和NodeJS基础系列文章,包括NodeJS入门、NodeJS模块导出与解析、NodeJSIOStream、NodeJSHTTPS。笔者之前在使用require导入模块的时候,尤其是导入有状态模块的时候,笔者会考虑在多次导入的情况下是否还保持单例特性,或者是否在不同路径下导入同一个文件。可以确定为一致吗?本文就是要分析这个特性。NodeJS模块默认是单例的,但并不保证一定是我们编程时想象的单例。根据NodeJS的官方文档,模块导入是否是单例取决于以下两个因素影响:Node模块的缓存机制是区分大小写的,例如require('/foo')和require('/FOO')将返回两个不同的对象,即使您的foo和FOO是完全相同的文档。模块根据其解析的文件名进行缓存。由于不同的模块将依赖于它们的调用路径来识别缓存,因此无法保证您将始终使用require('foo')返回相同的对象。也许根据不同的文件路径会得到不同的对象。创建一个新的NodeJS模块根据NodeJS文档,文件和模块之间存在一一对应关系。这也是解释上面提到的模块缓存机制的基础。我们首先创建一个简单的模块//counter.jsletvalue=0module.exports={increment:()=>value++,get:()=>value,}在counter.js中我们创建一个私有变量,它只能通过公共增量和获取方法。在应用中,我们可以这样使用这个模块://app.jsconstcounter=require('./counter.js')counter.increment()counter.increment()console.log(counter.get())//prints2console.log(counter.value)//printsundefinedasvalueisprivateModuleCachingNodeJS会在第一次导入模块后缓存它,官方文档中描述:每次调用require('foo')都会返回完全相同的对象,如果它将解析为同一个文件。我们也可以通过下面这个简单的例子来验证这句话://app-singleton.jsconstcounter1=require('./counter.js')constcounter2=require('./counter.js')counter1.increment()counter1.increment()counter2.increment()console.log(counter1.get())//prints3console.log(counter2.get())//也可以看到prints3虽然我们导入了两次模块,但它仍然指向同一个对象.但并不是每次我们导入相同的模块时,我们都会得到相同的对象。在NodeJS中,模块对象有一个内置方法:Module._resolveFilename(),负责在require中找到合适的模块。找到正确的文件后,它会将其文件名作为缓存的键名。官方的搜索算法伪代码为:require(X)frommoduleatpathY1.IfXisacoremodule,a.returnthecoremoduleb.STOP2.IfXbeginswith'./'or'/'or'../'a.LOAD_AS_FILE(Y+X)1.IfXisafile,loadXasJavaScripttext。STOP2.IfX.jsisafile,loadX.jsasJavaScripttext.STOP3...4...b.LOAD_AS_DIRECTORY(Y+X)1.IfX/package.jsonisafile,a.ParseX/package.json,然后寻找“main”字段。b.letM=X+(jsonmainfield)c.LOAD_AS_FILE(M)2.IfX/index.jsisafile,loadX/index.jsasJStext.STOP3...4...3.LOAD_NODE_MODULES(X,dirname(Y))4.THROW“notfound”简单来说,加载的逻辑或者说优先级是:先判断是不是核心模块,如果不是核心模块,再去搜索node_modules;否则,可根据模块对象或//counter-debug.jsconsole.log(module.filename)//printsabsolutepathtocounter.jsconsole.log(__filename)//printssameasabove/获取相对路径中查找解析后的文件名/iget:"/Users/laz/repos/medium/modules/counter-debug.js"letvalue=0module.exports={increment:()=>value++,get:()=>value,在上面的例子中,我们可以看到解析得到的文件名甚至是加载模块的绝对路径。根据文件和模块一一对应的原则,我们可以得出以下两种会破坏模块导入单例的特例。CaseSensitivity在区分大小写的文件系统或操作系统中,不同的解析文件可能指向同一个文件,但它们的缓存键名会不一致,即不同的导入会生成不同的对象。//app-no-singleton-1.jsconstcounter1=require('./counter.js')constcounter2=require('./COUNTER.js')counter1.increment()console.log(counter1.get())//prints1console.log(counter2.get())//prints0,notsameobjectascounter1/*我们有两个不同的解析文件名:-"Users/laz/repos/medium/modules/counter.js"-"Users/laz/repos/medium/modules/COUNTER.js"*/在上面的例子中,我们分别使用counter和COUNTER来导入不同大小写的同一个文件。如果是在区分大小写的系统中,比如UBUNTU,会直接抛出Exception:resolvedtoadifferentfilename当我们使用require(x)并且x不是核心模块的一部分时,它会自动搜索node_modules文件夹。在npm3之前,项目会以嵌套的方式安装依赖项。所以当我们的项目依赖module-a和module-b,并且module-a和module-b也相互依赖时,会生成如下文件路径格式://npm2installeddependenciesinnestedwayapp.jspackage.jsonnode_modules/|---module-a/index.js|---module-b/index.js|---node_modules|---module-a/index.js在这种情况下,我们有同一个模块的两个副本,那么当我们应用导入module-a时,如何加载如下文件://app.jsconstmoduleA=require('module-a')loads:"/node_modules/module-a/index.js"从module-b加载模块时-a,它加载以下文件:///node_modules/module-b/index.jsconstmoduleA=require('module-a')loads“/node_modules/module-b/node_modules/module-a/index.js”然而,在npm3之后,它以扁平的方式加载文件,其文件目录结构如下:index.js但是此时还有另外一种场景,也就是说,我们的应用本身依赖于module-a@v1.1和module-b,而module-b依赖于module-a@v1.2,在这种情况下,npm3之前类似的嵌套目录结构仍然会使用。这样的话,会为module-a生成不同的对象,但是此时是不同的文件,所以它们之间不会有冲突。