本文翻译自 Lazlojuly 的 are-node-js-modules-singletons。

本文从属于笔者的NodeJS入门与***实践中的NodeJS 基础系列文章,包括NodeJS 入门、NodeJS 模块导出与解析、NodeJS IOStream、NodeJS HTTPS这几部分。
笔者之前在使用require导入模块时,特别是在导入有状态的模块时,笔者会考虑其是否在多次导入情况下依然保持单例特性,或者说对于同一个文件在不同路径下导入时,是否能够识别为一致?本文即是对该特性进行解析。
NodeJS的模块默认情况下是单例性质的,不过其并不能保证如我们编程时设想的那样一定是单例,根据NodeJS的官方文档中描述,某个模块导入是否为单例受以下两个因素的影响:
创建新的NodeJS模块
根据NodeJS文档所述,文件和模块是一一对应的关系。这个也是解释上文提及的模块缓存机制的基础,我们首先创建一个简单的模块:
- // counter.js
 - let value = 0
 - module.exports = {
 - increment: () => value++,
 - get: () => value,
 - }
 
在counter.js中我们创建了某个私有变量,并且只能通过公共的increment与get方法进行操作。在应用中我们可以如下方法使用该模块:
- // app.js
 - const counter = require(‘./counter.js’)
 - counter.increment()
 - counter.increment()
 - console.log(counter.get()) // prints 2
 - console.log(counter.value) // prints undefined as value is private
 
Module Caching
NodeJS会在***次导入某个模块之后将该模块进行缓存,在官方文档中有如下描述:
Every call to require(‘foo’) will get exactly the same object returned, if it would resolve to the same file.
我们也可以通过如下简单的例子来验证这句话:
- // app-singleton.js
 - const counter1 = require(‘./counter.js’)
 - const counter2 = require(‘./counter.js’)
 - counter1.increment()
 - counter1.increment()
 - counter2.increment()
 - console.log(counter1.get()) // prints 3
 - console.log(counter2.get()) // also prints 3
 
可以看出尽管我们两次导入了该模块,但是还是指向了同一个对象。不过并不是每次我们导入同一个模块时,都会得到相同的对象。在NodeJS中,模块对象有个内置的方法:Module._resolveFilename(),其负责寻找require中合适的模块,在找到正确的文件之后,会根据其文件名作为缓存的键名。官方的搜索算法伪代码为:
- require(X) from module at path Y
 - 1. If X is a core module,
 - a. return the core module
 - b. STOP
 - 2. If X begins with './' or '/' or '../'
 - a. LOAD_AS_FILE(Y + X)
 - 1. If X is a file, load X as JavaScript text. STOP
 - 2. If X.js is a file, load X.js as JavaScript text. STOP
 - 3...
 - 4...
 - b. LOAD_AS_DIRECTORY(Y + X)
 - 1. If X/package.json is a file,
 - a. Parse X/package.json, and look for "main" field.
 - b. let M = X + (json main field)
 - c. LOAD_AS_FILE(M)
 - 2. If X/index.js is a file, load X/index.js as JS text. STOP
 - 3...
 - 4...
 - 3. LOAD_NODE_MODULES(X, dirname(Y))
 - 4. THROW "not found"
 
简单来说,加载的逻辑或者说优先级为:
解析之后的文件名可以根据module对象或得到:
- // counter-debug.js
 - console.log(module.filename) // prints absolute path to counter.js
 - console.log(__filename) // prints same as above
 - // i get: "/Users/laz/repos/medium/modules/counter-debug.js"
 - let value = 0
 - module.exports = {
 - increment: () => value++,
 - get: () => value,
 
在上述的例子中我们可以看出,解析得到的文件名即使被加载模块的绝对路径。而根据文件与模块一一映射的原则,我们可以得出下面两个会破坏模块导入单例性的特例。
Case Sensitivity
在大小写敏感的文件系统中或者操作系统中,不同的解析之后的文件可能会指向相同的文件,但是其缓存键名会不一致,即不同的导入会生成不同的对象。
- // app-no-singleton-1.js
 - const counter1 = require('./counter.js')
 - const counter2 = require('./COUNTER.js')
 - counter1.increment()
 - console.log(counter1.get()) // prints 1
 - console.log(counter2.get()) // prints 0, not same object as counter1
 - /*
 - We have two different resolved filenames:
 - - “Users/laz/repos/medium/modules/counter.js”
 - - “Users/laz/repos/medium/modules/COUNTER.js”
 - */
 
在上面的例子中,我们分别用counter、COUNTER这仅仅是大小写不同的方式导入相同的某个文件,如果是在某个大小写敏感的系统中,譬如UBUNTU中会直接抛出异常:
解析为不同的文件名
当我们使用require(x)并且x不属于核心模块时,其会自动搜索node_modules文件夹。而在npm3之前,项目会以嵌套的方式安装依赖。因此当我们的项目依赖module-a与module-b,并且module-a与module-b也相互依赖时,其会生成如下文件路径格式:
- // npm2 installed dependencies in nested way
 - app.js
 - package.json
 - node_modules/
 - |---module-a/index.js
 - |---module-b/index.js
 - |---node_modules
 - |---module-a/index.js
 
这样的话,我们对于同一个模块就有两个副本,那当我们在应用中导入module-a时,岂会载入如下文件:
- // app.js
 - const moduleA = require(‘module-a’)
 - loads: “/node_modules/module-a/index.js”
 
而从module-b中载入module-a时,其载入的是如下文件:
- // /node_modules/module-b/index.js
 - const moduleA = require(‘module-a’)
 - loads “/node_modules/module-b/node_modules/module-a/index.js”
 
不过在npm3之后,其以扁平化方式进行文件加载,其文件目录结构如下所示:
- // npm3 flattens secondary dependencies by installing in same folder
 - app.js
 - package.json
 - node_modules/
 - |---module-a/index.js
 - |---module-b/index.js
 
不过此时就存在另一个场景,即我们应用本身依赖module-a@v1.1与module-b,而module-b又依赖于module-a@v1.2,在这种情况下还是会采用类似于npm3之前的嵌套式目录结构。这样的话对于module-a一样会产生不同的对象,不过此时本身就是不同的文件了,因此相互之间不会产生冲突。
                名称栏目:NodeJS中的模块是单例的吗?
                
                链接地址:http://www.csdahua.cn/qtweb/news36/428386.html
            
网站建设、网络推广公司-快上网,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 快上网