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是否是相同的文件无关。
那么简单来说:
如果您需要一个Singleton,请导出一个对象。
如果您不需要一个Singleton,请导出一个函数(并在该函数中执行/返回等等)。
<注意>非常明确,如果您正确执行此操作,它应该有效,请参考https://dev59.com/ZmYs5IYBdhLWcg3wDfp5#33746703(Allen Luce的答案)。它通过代码解释了由于不同的解析文件名导致缓存失败时会发生什么。但是,如果您始终解析为相同的文件名,它应该有效。使用es6符号在node.js中创建真正的单例 另一种解决方案: 在此链接中
此答案涉及CommonJS(Node.js的自己的导入/导出模块方式)。Node.js很可能会转换到ECMAScript Modules: https://nodejs.org/api/esm.html (如果您不知道,ECMAScript是JavaScript的真实名称)
迁移到ECMAScript时,现在请阅读以下内容:https://nodejs.org/api/esm.html#esm_writing_dual_packages_while_avoiding_or_minimizing_hazards
编号. 当 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中使用单例模式。
进一步查看 Modules 文档中的 模块缓存注意事项:
模块是基于其已解析的文件名进行缓存的。由于模块可能基于调用模块的位置 (从 node_modules 文件夹加载) 解析为不同的文件名,因此如果解析到不同的文件,则 不能保证 require('foo') 总是返回完全相同的对象。
因此,根据您要求模块的位置不同,可能会获得不同实例的模块。
听起来像模块并不是创建单例的简单解决方案。
编辑: 或许它们是。就像 @mkoryak 一样,我无法想出一个单个文件可能解析为不同文件名的情况(不使用符号链接)。但是 (如 @JohnnyHK 所述),在不同的 node_modules
目录中存在多个文件副本,每个副本将被分别加载和存储。
node_modules
中要求两个不同的模块,每个模块依赖于相同的模块,但是在每个不同模块的node_modules
子目录下有单独的依赖模块副本时的情况。 - JohnnyHKrequire('./db')
在两个不同的文件中,
db
模块的代码会执行两次。 - willscriptedrequire('../lib/myModule.js');
,而在另一个文件中,我调用了 require('../lib/mymodule.js');
,但它们没有返回相同的对象。为此我感到非常困扰。 - heyarne....由于模块可能根据调用模块的位置解析为不同的文件名..
对我来说,这个语句意味着nodejs使用相对路径(而不是绝对路径)作为缓存键,因为它依赖于调用模块的位置。是这样吗? - emillyvar 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
may not
” 是在开发过程中使用 npm link
模块时适用的。因此,在使用依赖于单个实例(例如 eventBus)的模块时要小心。 - mediafreakch这里唯一使用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
方法来规避该问题。
summary
,而不需要重新初始化它? - M Faisal Hameed在JavaScript中实现单例不需要任何特殊的东西,这篇文章中的代码也可以这样写:
var socketList = {};
module.exports = {
add: function() {
},
...
};
如果在node.js之外(例如在浏览器的js中),您需要手动添加包装函数(在node.js中会自动完成):
var singleton = function() {
var socketList = {};
return {
add: function() {},
...
};
}();
在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();
需要注意的是,使用new
关键字并非必要。任何旧的对象、函数、IIFE等都可以工作-这里没有发生面向对象编程的神秘现象。
如果你在函数内部闭包一些对象,并使该函数成为全局函数,则即使重新分配全局变量,也不会覆盖已经从它创建的实例-尽管这可能并不实用。
module.exports = new Foo()
,因为 module.exports 不会再次执行,除非你做了一些非常愚蠢的事情。 - mkoryakmodule.exports = new class foo {...}
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); });