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

NodeJS中的模块是单例吗?

时间:2023-04-04 01:26:08 Node.js

本文由Lazlojuly翻译自are-node-js-modules-singletons。本文是作者NodeJS入门与最佳实践NodeJS基础系列的一部分,包括NodeJS入门、NodeJS模块导出与分析、NodeJSIOStream、NodeJSHTTPS。笔者之前在使用require导入模块的时候,尤其是导入有状态模块的时候,笔者会考虑在多次导入的情况下是否还保持单例特性,或者是否在不同路径下导入同一个文件。可以被认为是一致的吗?本文就是要分析这个特性。NodeJS模块默认是单例的,但并不保证一定是我们编程时想象的单例。根据NodeJS官方文档,一个模块import是否是单例,取决于以下两个因素影响: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())//打印3console.log(counter2.get())//也打印3可以看出,虽然我们导入了两次模块,它仍然指向同一个对象。但并不是每次我们导入相同的模块时,我们都会得到相同的对象。在NodeJS中,模块对象有一个内置方法:Module._resolveFilename(),负责在require中找到合适的模块。找到正确的文件后,它会将其文件名作为缓存的键名。官方的搜索算法伪代码是:require(X)frommoduleatpathY1。如果X是核心模块,则a.返回核心模块b。停止2。如果X以'./'或'/'或'.。/'A。LOAD_AS_FILE(Y+X)1.如果X是文件,则将X作为JavaScript文本加载。停止2.如果X.js是文件,则将X.js作为JavaScript文本加载。停止3...4...b。LOAD_AS_DIRECTORY(Y+X)1.如果X/package.json是一个文件,a。解析X/package.json,并查找“main”字段。b.让M=X+(json主字段)c。LOAD_AS_FILE(M)2.如果X/index.js是一个文件,将X/index.js加载为JS文本。停止3...4...3。LOAD_NODE_MODULES(X,dirname(Y))4.THROW"notfound"简单来说,加载的逻辑或者说优先级是:先判断是否是核心模块,如果不是核心模块,再搜索node_modules;否则,在相对路径中搜索解析后的文件名可以根据模块对象或get://counter-debug.jsconsole.log(module.filename)//打印绝对路径到counter.jsconsole.log(__filename))//打印与上面相同//我得到:“/Users/laz/repos/medium/modules/counter-debug.js”letvalue=0module.exports={incrment:()=>value++,get:()=>value,在上面的例子中,我们可以看到,即使解析得到的文件名是加载模块的绝对路径,按照一到的原则-one文件和模块之间的映射,我们可以得出以下两种特殊情况会打破模块导入的单例。CaseSensitivity在区分大小写的文件系统或操作系统中,不同的解析文件可能指向同一个文件,但它们的缓存键名会不一致,即不同的导入会生成不同的对象。//app-no-singleton-1.jsconstcounter1=require('./counter.js')constcounter2=require('./COUNTER.js')counter1.increment()console.log(counter1.get())//打印1console.log(counter2.get())//打印0,与counter1不是同一个对象/*我们有两个不同的解析文件名:-“Users/laz/repos/medium/modules/counter.js”-"Users/laz/repos/medium/modules/COUNTER.js"*/在上面的例子中,我们分别使用counter和COUNTER以不同的方式导入同一个文件,如果是在某个大小的一个write敏感系统,比如UBUNTU,会直接抛出异常:resolvetoadifferentfilename当我们使用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"from当module-a加载到module-b时,会加载以下文件:///node_modules/module-b/index.jsconstmoduleA=require('module-a')loads“/node_modules/module-b/node_modules/module-a/index.js"但是在npm3之后是扁平化加载文件,其文件目录结构如下://npm3flattenssecondarydependenciesbyinstalledinsamefolderapp.jspackage.jsonnode_modules/|---module-a/index.js|---module-b/index.js但是这个时候还有另外一个场景,就是我们的应用本身依赖于module-a@v1.1和module-b,而module-b它也依赖于module-a@v1.2。在这种情况下,仍然会使用类似于npm3之前的嵌套目录结构。这样的话,会为module-a生成不同的对象,但是此时是不同的文件,所以它们之间不会有冲突。