Node.js中的Singleton模式-是否需要?


95

最近,我碰上了这篇文章如何写在Node.js的一个单 我知道require 状态文档:

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

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

题:

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


1
这是5分钟。关于此主题的说明(在v6和npm3之后编写):medium.com/@lazlojuly/…–
lazlojuly

Answers:


56

这基本上与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

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

更新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


3
如果要单身人士;出口的对象...这有助于感谢
danday74

1
这是一个坏主意-出于本页其他地方给出的许多原因-但该概念实质上是有效的,也就是说,在确定的名义环境下,此答案中的主张是正确的。如果您想要快速又脏的单身人士,这可能会起作用-只是不要用代码启动任何班车。
亚当·托利

@AdamTolley“由于此页面上其他地方给出的许多原因”,您是指符号链接文件还是拼写错误的文件名,这些文件名显然没有使用相同的缓存?它确实在文档中说明了不区分大小写的文件系统或操作系统的问题。关于符号链接,您可以在这里阅读更多内容,如github.com/nodejs/node/issues/3402所讨论。另外,如果您正在符号链接文件或不正确地理解您的操作系统和节点,那么您不应该在航空航天工程行业附近;),但是我确实理解您的意思^^。
K-SO的毒性在增加。

@KarlMorrison-仅出于文档不能保证它的事实,它似乎是未指定的行为或任何其他不信任该语言的合理原因的事实。也许缓存在另一个实现中的工作方式有所不同,或者您喜欢在REPL中工作,并且完全颠覆了缓存功能。我的观点是,缓存是实现细节,它与单例等效项是一个聪明的技巧。我喜欢聪明的骇客,但应该加以区分,仅此而已-(也没有人发动Node航天飞机,我很傻)
Adam Tolley

136

以上所有内容都过于复杂。有一种流派认为设计模式显示出实际语言的不足。

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

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

但是,是的,如果您想使用共享对象,请将其放在模块导出中就可以了。只是不要使其与“单一模式”复杂化,而在JavaScript中则不需要它。


27
很奇怪,没有人投票赞成... +1了There is a school of thought which says design patterns are showing deficiencies of actual language.
Esailija 2012年

64
单例不是反模式。
wprl 2013年

4
@herby,似乎对单例模式的定义过于具体(因此不正确)。
wprl

19
该文档显示为:“多次调用require('foo')可能不会导致模块代码多次执行。”。它说“可能”,不是说“不会”,所以从我的角度来看,问如何确保在应用程序中仅创建一次模块实例是一个有效的问题。
xorcus

9
这是对这个问题的正确答案,这是令人误解的。就像@mike在下面指出的那样,一个模块可能加载了不止一次,而您有两个实例。我遇到的问题是,我只有一个Knockout副本,但是创建了两个实例,因为该模块加载了两次。
dgaviola 2015年

26

否。 当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

然后将sg2require行更改为:

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

,单身被击败:

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


21

再看一下Modules文档中的Module Caching注意事项

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

因此,根据需要模块的位置,可以获取模块的其他实例。

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

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


好的,我读了3次,但我仍然无法考虑将其解析为其他文件名的示例。救命?
mkoryak '11年

1
@mkoryak我认为这是指您需要两个不同模块的情况node_modules,每个模块都依赖于同一模块,但是node_modules在两个不同模块的每个子目录下都有该依赖模块的单独副本。
JohnnyHK

@mike你就在这里,当通过不同的路径引用时,模块会被多次实例化。在为服务器模块编写单元测试时遇到了这种情况。我需要一种单例实例。怎么实现呢?
Sushil 2013年

一个例子可能是相对路径。例如。给定require('./db')在两个单独的文件中,该db模块的代码执行两次
2013年

9
因为节点模块系统不区分大小写,所以我只是遇到了一个讨厌的错误。我require('../lib/myModule.js');在一个文件中调用了另一个文件,require('../lib/mymodule.js');但没有传递相同的对象。
heyarne 2014年

18

这样就完全不需要在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

5
文档说“ 可能不会导致模块代码多次执行”,因此有可能被多次调用,如果再次执行此代码,则socketList将被重置为空列表
Jonathan。

3
@乔纳森 上下文中的文档周围的报价似乎使一个非常有说服力的情况下可能不会是在RFC风格的使用不得
迈克尔

6
@迈克尔“可能”是一个很有趣的词。花哨的有一个单词,当被否定时意味着“也许不是”或“绝对不是”
。.– OJFord

1
may not适用于当你npm link在开发过程中的其他模块。因此,在使用依赖于单个实例的模块(例如eventBus)时要小心。
mediafreakch '16

10

这里唯一使用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方法来避免这种情况。


问题是“需要单身人士”,而不是“你怎么写单身人士”
mkoryak 17-10-13

@ danday74我们如何summary在另一个类中使用同一实例,而无需再次对其进行初始化?
M Faisal Hameed

1
在Node.js中,只需要在另一个文件中... const summary = require('./ SummaryModule')...它将是同一实例。您可以通过创建成员变量并在需要它的一个文件中设置其值,然后在需要它的另一个文件中获取其值来进行测试。它应该是设置的值。
danday74 '18

9

您不需要任何特殊操作即可在js中执行单例操作,本文中的代码也可以是:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};

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

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

正如@Allen Luce指出的,如果节点的缓存失败,则单例模式也会失败。
rakeen

6

Singleton在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关键字不是必需的。任何旧的对象,功能,示例等都可以使用-这里没有发生OOP伏都教。

如果您在函数中关闭某个obj并返回对该函数的引用并使其成为全局函数,则可以加分-即便重新分配全局变量也不会破坏已经从其创建的实例-尽管这样做很有用。


您不需要任何。您可以这样做module.exports = new Foo()是因为module.exports不会再次执行,除非您做的非常愚蠢
mkoryak

您绝对不应依赖于实现方面的副作用。如果需要单个实例,只需将其绑定到全局实例,以防实现发生变化。
亚当·托利

上面的答案也是对原始问题的误解:“我应该在JS中使用单例还是该语言使它们成为不必要?”,这似乎也与许多其他答案有关。我支持我的建议,反对使用require实现代替适当的显式单例实现。
亚当·托利

1

保持简单。

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); });

问题是“需要单身人士”,而不是“你怎么写单身人士”
mkoryak
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.