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

mko*_*yak 86 javascript singleton design-patterns node.js

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

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

因此,似乎每个必需的模块都可以很容易地用作单例,而不需要单独的样板代码.

题:

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

小智 134

所有上述内容都过于复杂.有一种思想流派认为设计模式显示出实际语言的缺陷.

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

至于节点中的模块,是的,默认情况下它们是缓存的,但是如果你想热插入模块更改,可以调整它.

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

  • 单身人士不是反模式. (60认同)
  • 奇怪的是,没有人能够获得赞成...有一个+1的"有一种思想流派认为设计模式显示出实际语言的缺陷". (26认同)
  • 文档中写道:"多次调用require('foo')**可能不会导致模块代码多次执行." 它说"可能没有",它没有说"不会",所以询问如何确保模块实例在应用程序中只创建一次是一个有效的问题,从我的角度来看. (17认同)
  • 这是这个问题的正确答案,这是误导性的.正如@mike在下面指出的那样,模块可能会被多次加载而你有两个实例.我正在遇到这个问题,我只有一个Knockout副本但创建了两个实例,因为模块加载了两次. (9认同)
  • @herby,似乎是对单例模式的过度特定(因此也是不正确的)定义. (3认同)
  • 如果我正确地阅读了这个答案,那么问题的唯一解决方案就是依赖于模块的一次加载功能.我不明白单个对象如何在一个模块中"在运行中"创建,否则另一个模块可以访问.或者你是说这个问题不应该用"单身人士"这个词来表达?如果是后者,哪个术语更适合node.js中多个模块可访问的单个资源?我担心我会以同样的方式提出这个问题. (2认同)
  • 此非答案忽略/错误是一个单例。单例是跨越多个上下文的单个且相同的对象(引用),无法根据定义即时创建。单例在各种实例中很有用,并且可以通过js中的各种方式来实现,其中任何一种都仅需要确保对一个对象的所有引用都是相同的。这样的例子就是依靠节点的习惯来缓存旧对象的,即使不是一个好主意,_concept_仍然有效。这里提供的其他解决方案可能是一个更好的主意。 (2认同)

Kar*_*son 41

这基本上与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是否是同一个文件.

所以简单来说.

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

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

非常清楚,如果你这样做,它应该工作,看看/sf/answers/2362269241/(Allen Luce的回答).它在代码中解释了当缓存由于不同解析的文件名而失败时会发生什么.但如果你总是解析相同的文件名,它应该工作.

  • 如果你想要一个单身人士;导出一个对象...帮助谢谢 (4认同)
  • 这是一个坏主意 - 由于本页其他地方给出的许多原因 - 但_概念_本质上是有效的,也就是说,在既定的名义情况下,这个答案中的主张是正确的。如果你想要一个快速而肮脏的单例,这可能会起作用 - 只是不要用代码启动任何航天飞机。 (2认同)
  • @KarlMorrison - 只是因为文档不能保证这一点,事实上它似乎是未指定的行为,或者不信任该语言的这种特定行为的任何其他合理原因。也许缓存在另一个实现中的工作方式不同,或者您喜欢在 REPL 中工作并完全颠覆缓存功能。我的观点是,缓存是一个实现细节,将其用作单例等效项是一个聪明的技巧。我喜欢聪明的黑客,但它们应该有所区别,仅此而已 - (也没有人用节点发射航天飞机,我很傻) (2认同)

All*_*uce 23

当节点的缓存模块发生故障,该单例模式失败.我修改了示例以在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());
Run Code Online (Sandbox Code Playgroud)

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

{ '1': 'test', '2': 'test2' } { '1': 'test', '2': 'test2' }
Run Code Online (Sandbox Code Playgroud)

但是一个小修改就会破坏缓存.在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());
Run Code Online (Sandbox Code Playgroud)

或者,在Linux上:

% ln singleton.js singleton2.js
Run Code Online (Sandbox Code Playgroud)

然后将sg2require行更改为:

var sg2 = require("./singleton2.js");
Run Code Online (Sandbox Code Playgroud)

,单身被击败:

{ '1': 'test' } { '2': 'test2' }
Run Code Online (Sandbox Code Playgroud)

我不知道一种可以接受的解决方法.如果你真的觉得需要制作类似单身的东西并且可以污染全局命名空间(以及可能导致的许多问题),你可以将作者getInstance()exports行改为:

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

module.exports = singleton.getInstance();
Run Code Online (Sandbox Code Playgroud)

也就是说,我从来没有遇到生产系统的情况,我需要做这样的事情.我也从未觉得需要在Javascript中使用单例模式.


mik*_*ike 20

进一步了解模块文档中的模块缓存注意事项:

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

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

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

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

  • 我只是有一个讨厌的错误,因为节点模块系统不区分大小写.我在一个文件中调用`require('../ lib/myModule.js');`和`require('../ lib/mymodule.js');`在另一个文件中``它没有传递相同的对象. (9认同)

Pau*_*ong 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
Run Code Online (Sandbox Code Playgroud)

  • @Michael"may"是一个有趣的词.想要说一句话,否定意味着"也许不是"或"绝对不是"...... (6认同)
  • 文档说"*可能不会导致模块代码多次执行",因此可能会多次调用它,如果再次执行此代码,则socketList将重置为空列表 (4认同)
  • @Jonathan.围绕该引用的上下文[在文档中](http://nodejs.org/api/modules.html)似乎提出了一个非常有说服力的案例,即_may not_用于RFC风格的_MUST NOT_. (3认同)

Esa*_*ija 9

在js中你不需要任何特殊的单例,文章中的代码也可以是:

var socketList = {};

module.exports = {
      add: function() {

      },

      ...
};
Run Code Online (Sandbox Code Playgroud)

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

var singleton = function() {
    var socketList = {};
    return {
        add: function() {},
        ...
    };
}();
Run Code Online (Sandbox Code Playgroud)


Ada*_*ley 7

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

在 node 中,如果您需要单例,例如在服务器层的各种文件中使用相同的 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();
Run Code Online (Sandbox Code Playgroud)

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

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


dan*_*y74 7

这里唯一使用ES6类的答案

// SummaryModule.js
class Summary {

  init(summary) {
    this.summary = summary
  }

  anotherMethod() {
    // do something
  }
}

module.exports = new Summary()
Run Code Online (Sandbox Code Playgroud)

要求这个单身人士:

const summary = require('./SummaryModule')
summary.init(true)
summary.anotherMethod()
Run Code Online (Sandbox Code Playgroud)

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