【问题标题】:Singleton pattern in nodejs - is it needed?nodejs中的单例模式 - 需要吗?
【发布时间】:2012-10-22 03:51:04
【问题描述】:

我最近遇到了this article,了解如何在 Node.js 中编写单例。我知道requirestates的文档是:

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

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

问题:

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

【问题讨论】:

标签: javascript node.js design-patterns singleton


【解决方案1】:

以上所有内容都过于复杂。有一种观点认为设计模式显示出实际语言的缺陷。

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

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

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

【讨论】:

  • 奇怪的是没有人获得支持...为There is a school of thought which says design patterns are showing deficiencies of actual language.+1
  • 单例不是反模式。
  • @herby,似乎对单例模式的定义过于具体(因此不正确)。
  • 文档中写道:“多次调用 require('foo') 可能不会导致模块代码被多次执行。”。它说“可能不会”,它没有说“不会”,所以从我的角度来看,询问如何确保模块实例在应用程序中只创建一次是一个有效的问题。
  • 这是该问题的正确答案,这是一种误导。正如@mike 在下面指出的那样,一个模块可能会被多次加载并且您有两个实例。我遇到了一个问题,我只有一个 Knockout 副本,但由于模块加载了两次,所以创建了两个实例。
【解决方案2】:

这基本上与 nodejs 缓存有关。简单明了。

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

(v 6.3.1)

缓存

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

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

如果你想让一个模块多次执行代码,那么导出 一个函数,并调用该函数。

模块缓存注意事项

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

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

简单来说。

如果你想要一个单例; 导出对象

如果你不想要单例; 导出函数(并在该函数中执行操作/返回操作/任何操作)。

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

2016 年更新

creating a true singleton in node.js with es6 symbols 另一种解决方案in this link

2020 年更新

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

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

【讨论】:

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

否。当 Node 的模块缓存失败时,单例模式就会失败。我修改了示例以在 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

然后将sg2 require 行更改为:

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 中使用单例模式。

【讨论】:

  • 读到这里很有趣。然而,最终的结论是:你必须故意破坏你的代码(在这种情况下是节点缓存机制)来破坏 Node.JS 中的单例模式!
  • 虽然我采取了慎重的步骤来演示可以违反单例假设的一种方式,但不能保证在程序员不打算这样做的情况下不会出现类似的情况。
【解决方案4】:

进一步查看模块文档中的 Module Caching Caveats

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

因此,根据您在需要模块时所处的位置,可以获得模块的不同实例。

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

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

【讨论】:

  • 好的,我读了 3 次,但我仍然想不出一个可以解析为不同文件名的示例。帮忙?
  • @mkoryak 我认为这是指您需要从node_modules 获得两个不同模块的情况,其中每个模块都依赖于同一个模块,但该依赖模块有单独的副本node_modules 两个不同模块各自的子目录。
  • @mike 你就在这里,当通过不同的路径引用时,模块会被多次实例化。我在为服务器模块编写单元测试时遇到了这种情况。我需要一种单例实例。如何实现?
  • 一个例子可能是相对路径。例如。鉴于require('./db') 在两个单独的文件中,db 模块的代码会执行两次
  • 我刚刚遇到了一个讨厌的错误,因为节点模块系统是不区分大小写的。我在一个文件中调用了require('../lib/myModule.js');,在另一个文件中调用了require('../lib/mymodule.js');,但它没有传递相同的对象。
【解决方案5】:

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

由于模块是缓存和有状态的,因此您提供的链接上给出的示例可以很容易地重写得更简单:

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

【讨论】:

  • 文档说“可能不会导致模块代码被多次执行”,所以有可能会被多次调用,如果再次执行这段代码, socketList 将被重置为空列表
  • @Jonathan。该引用周围的上下文in the docs 似乎是一个非常令人信服的案例,即可能不会被用于RFC风格的MUST NOT
  • @Michael "may" 就是这样一个有趣的词。幻想有一个词,当否定时意味着“也许不是”或“绝对不是”..
  • may not 适用于您在开发过程中npm link 其他模块。因此,在使用依赖于单个实例(例如 eventBus)的模块时要小心。
【解决方案6】:

在js中做单例不需要什么特别的,文中的代码也可以是:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

在node.js之外(比如浏览器js中),需要手动添加wrapper函数(node.js中自动完成):

var singleton = function() {
    var socketList = {};
    return {
        add: function() {},
        ...
    };
}();

【讨论】:

  • 正如@Allen Luce 所指出的,如果节点的缓存失败,单例模式也会失败。
【解决方案7】:

这里唯一使用 ES6 类的答案

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()

需要这个单例:

const summary = require('./SummaryModule')
summary.init(true)
summary.anotherMethod()

这里唯一的问题是您不能将参数传递给类构造函数,但可以通过手动调用init 方法来规避。

【讨论】:

  • 问题是“需要单身人士”,而不是“你怎么写”
  • @danday74 我们如何在另一个类中使用相同的实例summary,而无需再次初始化它?
  • 在 Node.js 中只需要在另一个文件中使用它... const summary = require('./SummaryModule') ... 它会是同一个实例。您可以通过创建一个成员变量并将其值设置在一个需要它的文件中,然后在另一个需要它的文件中获取它的值来测试这一点。它应该是设置的值。
【解决方案8】:

单例在 JS 中很好,它们只是不需要那么冗长。

在节点中,如果您需要一个单例,例如在服务器层的各种文件中使用相同的 ORM/DB 实例,您可以将引用填充到全局变量中。

只需编写一个模块,如果全局变量不存在,则创建它,然后返回对它的引用。

@allen-luce 在此处复制了他的脚注代码示例:

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

module.exports = singleton.getInstance();

但重要的是要注意使用new 关键字不是必需的。任何旧的对象、函数、iife 等都可以工作——这里不会发生 OOP 巫术。

如果你在一个返回引用的函数中关闭某个 obj 并将该函数设为全局函数,那么即使重新分配全局变量也不会破坏已经从它创建的实例——尽管这是有问题的有用。

【讨论】:

  • 你不需要这些。你可以这样做 module.exports = new Foo() 因为 module.exports 不会再次执行,除非你做了一些非常愚蠢的事情
  • 你绝对不应该依赖实现的副作用。如果您需要单个实例,只需将其绑定到全局,以防实现发生变化。
  • 上述答案也是对原始问题的误解,即“我应该在 JS 中使用单例还是语言使它们变得不必要?”,这似乎也是很多其他问题答案。我坚持我的建议,反对使用 require 实现来替代适当的、显式的单例实现。
【解决方案9】:

保持简单。

foo.js

function foo() {

  bar: {
    doSomething: function(arg, callback) {
      return callback('Echo ' + arg);
    };
  }

  return bar;
};

module.exports = foo();

那么就

var foo = require(__dirname + 'foo');
foo.doSomething('Hello', function(result){ console.log(result); });

【讨论】:

  • 问题是“需要单身人士”,而不是“你怎么写”
【解决方案10】:

如果你想使用类, 是最短最漂亮的

module.exports = new class foo {...}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-03
    • 2010-10-03
    • 1970-01-01
    • 2019-07-30
    • 2018-06-11
    • 1970-01-01
    • 2018-10-19
    相关资源
    最近更新 更多