nodejs 中的单例模式 - 是否需要?

IT技术 javascript node.js design-patterns singleton
2021-02-05 21:33:07

我最近看到了这篇关于如何在 Node.js 中编写单例的文章。我知道的文档require 的状态是:

module在第一次加载后被缓存。多次调用require('foo')可能不会导致module代码被多次执行。

因此,似乎每个必需的module都可以轻松地用作单例,而无需单例样板代码。

问题:

上面的文章是否提供了创建单例的全面解决方案?

6个回答

以上这些都过于复杂了。有一种学派认为设计模式显示了实际语言的缺陷。

具有基于原型的 OOP(无类)的语言根本不需要单例模式。您只需动态创建一个(吨)对象,然后使用它。

至于节点中的module,是的,默认情况下它们是缓存的,但是如果您想要热加载module更改,则可以对其进行调整。

但是是的,如果你想全部使用共享对象,把它放在一个module导出中就可以了。只是不要将它与“单例模式”复杂化,在 JavaScript 中不需要它。

这是误导性的,这是这个问题的正确答案。正如@mike 在下面指出的那样,一个module可能被多次加载并且您有两个实例。我遇到了这个问题,我只有一个 Knockout 副本,但由于module加载了两次而创建了两个实例。
2021-03-25 21:33:07
@herby,似乎对单例模式的定义过于具体(因此不正确)。
2021-03-28 21:33:07
文档中写道:“多次调用 require('foo')可能不会导致module代码被多次执行。”。它说“可能不会”,它没有说“不会”,因此从我的角度来看,询问如何确保module实例仅在应用程序中创建一次是一个有效的问题。
2021-04-01 21:33:07
很奇怪没有人得到upvotes...有一个+1 There is a school of thought which says design patterns are showing deficiencies of actual language.
2021-04-09 21:33:07
单例不是反模式。
2021-04-10 21:33:07

这基本上与 nodejs 缓存有关。干净利落。

https://nodejs.org/api/modules.html#modules_caching

(v 6.3.1)

缓存

module在第一次加载后被缓存。这意味着(除其他外)每次调用 require('foo') 都会返回完全相同的对象,如果它解析为相同的文件。

多次调用 require('foo') 可能不会导致module代码被多次执行。这是一个重要的特征。有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。

如果你想让一个module多次执行代码,那么导出一个函数,然后调用那个函数。

module缓存注意事项

module根据其解析的文件名进行缓存。由于module可能会根据调用module的位置(从 node_modules 文件夹加载)解析为不同的文件名,因此不保证 require('foo') 将始终返回完全相同的对象,如果它解析为不同的文件.

此外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向同一个文件,但缓存仍会将它们视为不同的module,并会多次重新加载文件。例如, require('./foo') 和 require('./FOO') 返回两个不同的对象,无论 ./foo 和 ./FOO 是否是同一个文件。

所以简单来说。

如果你想要一个单身人士;导出一个对象

如果您不想要单身人士;导出一个函数(并在该函数中做东西/返回东西/任何东西)。

非常清楚,如果您正确执行此操作,它应该可以工作,请查看https://stackoverflow.com/a/33746703/1137669(Allen Luce 的回答)。它在代码中解释了由于不同解析的文件名导致缓存失败时会发生什么。但是如果你总是解析为相同的文件名,它应该可以工作。

2016 年更新

在 node.js 中用 es6 符号创建一个真正的单例 另一个解决方案在这个链接中

2020 年更新

这个答案是指CommonJS(Node.js 自己的导入/导出module的方式)。Node.js 很可能会切换到ECMAScript modulehttps : //nodejs.org/api/esm.html (如果您不知道,ECMAScript 是 JavaScript 的真实名称)

迁移到 ECMAScript 时,请暂时阅读以下内容:https : //nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards

这是一个坏主意——出于本页其他地方给出的许多原因——但这个概念本质上是有效的,也就是说,在既定的名义情况下,这个答案中的主张是正确的。如果你想要一个快速而肮脏的单身人士,这可能会奏效——只是不要用代码启动任何穿梭机。
2021-03-15 21:33:07
@KarlMorrison - 只是因为文档不保证这一事实,它似乎是未指定的行为,或者不信任该语言的这种特定行为的任何其他合理原因。也许缓存在另一个实现中的工作方式不同,或者您喜欢在 REPL 中工作并完全颠覆缓存功能。我的观点是缓存是一个实现细节,将它用作单例等效项是一个聪明的技巧。我喜欢聪明的黑客,但它们应该有所区别,仅此而已 - (也没有人用 node 发射航天飞机,我太傻了)
2021-03-26 21:33:07
如果你想要一个单身人士;导出一个对象...帮助谢谢
2021-03-28 21:33:07
@AdamTolley“出于本页其他地方给出的多种原因”,您是指符号链接文件还是拼写错误的文件名,这些文件显然没有使用相同的缓存?它确实在文档中说明了有关不区分大小写的文件系统或操作系统的问题。关于符号链接,您可以在此处阅读更多内容,因为它在github.com/nodejs/node/issues/3402 中进行了讨论此外,如果您对文件进行符号链接或不正确理解您的操作系统和节点,那么您不应该靠近航空航天工程行业 ;),但我确实理解您的观点^^。
2021-04-12 21:33:07

当节点的缓存module发生故障,该单例模式失败。我修改了示例以在 OSX 上有意义地运行:

var sg = require("./singleton.js");
var sg2 = require("./singleton.js");
sg.add(1, "test");
sg2.add(2, "test2");

console.log(sg.getSocketList(), sg2.getSocketList());

这给出了作者预期的输出:

{ '1': 'test', '2': 'test2' } { '1': 'test', '2': 'test2' }

但是一个小的修改会破坏缓存。在 OSX 上,执行以下操作:

var sg = require("./singleton.js");
var sg2 = require("./SINGLETON.js");
sg.add(1, "test");
sg2.add(2, "test2");

console.log(sg.getSocketList(), sg2.getSocketList());

或者,在 Linux 上:

% ln singleton.js singleton2.js

然后将sg2require 行更改为:

var sg2 = require("./singleton2.js");

bam,单身人士被击败:

{ '1': 'test' } { '2': 'test2' }

我不知道有什么可接受的方法来解决这个问题。如果你真的觉得需要做一些类似单例的东西并且可以污染全局命名空间(以及可能导致的许多问题),你可以将作者getInstance()exports更改为:

singleton.getInstance = function(){
  if(global.singleton_instance === undefined)
    global.singleton_instance = new singleton();
  return global.singleton_instance;
}

module.exports = singleton.getInstance();

也就是说,我从未在生产系统上遇到过需要执行此类操作的情况。我也从未觉得需要在 Javascript 中使用单例模式。

进一步查看module文档中的module缓存警告

module根据其解析的文件名进行缓存。由于module可能会根据调用module的位置(从 node_modules 文件夹加载)解析为不同的文件名,因此不保证require('foo') 将始终返回完全相同的对象,如果它解析为不同的文件.

因此,根据您在需要module时所处的位置,可能会获得该module的不同实例。

听起来像module不是创建单例的简单解决方案。

编辑:或者也许他们. 像@mkoryak 一样,我无法想出单个文件可能解析为不同文件名的情况(不使用符号链接)。但是(正如@JohnnyHK 评论的那样),不同node_modules目录中文件的多个副本将分别加载和存储。

@mkoryak 我认为这是指您需要两个不同的modulenode_modules,每个module都依赖于同一个module,但是node_modules在两个不同module的每个子目录下都有该依赖module的单独副本
2021-03-14 21:33:07
好的,我读了 3 次,但我仍然无法考虑一个可以解析为不同文件名的示例。帮助?
2021-03-16 21:33:07
由于节点module系统不区分大小写,因此我遇到了一个令人讨厌的错误。我调用require('../lib/myModule.js');了一个文件和require('../lib/mymodule.js');另一个文件,但它没有提供相同的对象。
2021-03-23 21:33:07
@mike 您就在这里,当通过不同路径引用时,module会被多次实例化。我在为服务器module编写单元测试时遇到了这种情况。我需要一种单例实例。如何实现呢?
2021-03-26 21:33:07
一个例子可能是相对路径。例如。鉴于require('./db')在两个单独的文件中,dbmodule的代码执行两次
2021-04-01 21:33:07

node.js(或浏览器 JS,就此而言)中的单例是完全没有必要的。

由于module是缓存和有状态的,您提供的链接中给出的示例可以轻松地更简单地重写:

var socketList = {};

exports.add = function (userId, socket) {
    if (!socketList[userId]) {
        socketList[userId] = socket;
    }
};

exports.remove = function (userId) {
    delete socketList[userId];
};

exports.getSocketList = function () {
    return socketList;
};
// or
// exports.socketList = socketList
文档说“可能不会导致module代码被多次执行”,所以有可能会被多次调用,如果再次执行这段代码,socketList会被重置为空列表
2021-03-14 21:33:07
may not适用npm link于开发期间的其他module。因此,在使用依赖于单个实例(例如 eventBus)的module时要小心。
2021-03-19 21:33:07
@迈克尔“可能”是一个有趣的词。喜欢有一个词,当否定时意味着“也许不是”或“绝对不是”。
2021-03-28 21:33:07
@乔纳森。围绕该引用的文档中的上下文似乎是一个非常有说服力的案例,可能未在 RFC 样式中使用MUST NOT
2021-04-12 21:33:07