如何处理Node.js中的循环依赖

Run*_*ble 148 module require cyclic-reference node.js cyclic-dependency

我最近一直在使用nodejs并且仍然掌握模块系统,所以如果这是一个显而易见的问题,请道歉.我想要的代码大致如下所示:

a.js(主节点与节点一起运行)

var ClassB = require("./b");

var ClassA = function() {
    this.thing = new ClassB();
    this.property = 5;
}

var a = new ClassA();

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

b.js

var a = require("./a");

var ClassB = function() {
}

ClassB.prototype.doSomethingLater() {
    util.log(a.property);
}

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

我的问题似乎是我无法从ClassB的实例中访问ClassA的实例.

是否有正确/更好的方法来构建模块以实现我想要的?有没有更好的方法在模块之间共享变量?

lan*_*nzz 170

尝试设置属性module.exports,而不是完全替换它.例如,module.exports.instance = new ClassA()in a.js,module.exports.ClassB = ClassBin b.js.当您创建循环模块依赖项时,需求模块将从所需模块获得对不完整的引用module.exports,您可以在后面添加其他属性,但是当您设置整个时module.exports,实际上创建了一个新对象,其中需求模块没有访问方式.

  • 这可能都是正确的,但我会说仍然避免循环依赖.做出特殊安排来处理未完全加载的模块听起来会产生你不想要的未来问题.这个答案规定了如何处理未完成的模块的解决方案......我认为这不是一个好主意. (4认同)

Joh*_*yHK 79

虽然node.js确实允许循环require依赖,但是你发现它可能非常混乱,你可能最好不要重构代码而不需要它.也许创建第三个类,使用其他两个来完成你需要的东西.

  • 不总是.例如,在数据库模型中,如果我有模型A和B,则在模型AI中可能想要引用模型B(例如,加入操作),反之亦然.因此,在使用"require"函数之前导出多个A和B属性(不依赖于其他模块的属性)可能是一个更好的答案. (81认同)
  • 我也没有看到循环依赖作为代码气味.我正在开发一个系统,其中有一些需要它的情况.例如,建模团队和用户,其中用户可以属于许多团队.所以,我的建模并没有出错.显然,我可以重构我的代码以避免两个实体之间的循环依赖,但这不是域模型中最纯粹的形式,所以我不会这样做. (10认同)
  • +1这是正确的答案.循环依赖是代码气味.如果A和B总是一起使用,则它们实际上是单个模块,因此将它们合并.或者找到一种打破依赖的方法; 也许它是一个复合模式. (5认同)
  • 这并不乱.. 有人可能想要刹车一个文件以避免一本代码书 ia 单个文件。正如 node 建议的那样,您应该在代码的顶部添加一个 `exports = {}`,然后在代码的末尾添加一个 `exports = yourData`。通过这种做法,您将避免几乎所有来自循环依赖的错误。 (2认同)
  • 两个模块可能相互依赖的另一个合理原因是:它们是由不同的作者编写的,并且应该保留单独的文件,因为它们在系统中具有不同的角色。 (2认同)

Wil*_*ern 48

[编辑]它不是2015年,大多数图书馆(即快递)都使用更好的模式进行更新,因此不再需要循环依赖.我建议不要使用它们.


我知道我在这里挖掘一个旧的答案......这里的问题是你需要ClassB 之后定义了module.exports .(JohnnyHK的链接显示)循环依赖在Node中运行良好,它们只是同步定义.如果使用得当,它们实际上解决了很多常见的节点问题(比如app从其他文件访问express.js )

只需确保在需要具有循环依赖关系的文件之前定义了必要的导出.

这将打破:

var ClassA = function(){};
var ClassB = require('classB'); //will require ClassA, which has no exports yet

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

这将有效:

var ClassA = module.exports = function(){};
var ClassB = require('classB');
Run Code Online (Sandbox Code Playgroud)

我一直使用这种模式访问app其他文件中的express.js :

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app
Run Code Online (Sandbox Code Playgroud)

  • 感谢您分享该模式,然后进一步分享您在导出 `app = express()` 时通常如何使用该模式 (2认同)

Coe*_*oen 32

有时引入第三个类(如JohnnyHK建议的那样)是非常人为的,所以除了Ianzz之外:如果你确实想要替换module.exports,例如你正在创建一个类(比如b.js文件)上面的例子),这也是可能的,只要确保在启动循环要求的文件中,'module.exports = ...'语句在require语句之前发生.

a.js(主节点与节点一起运行)

var ClassB = require("./b");

var ClassA = function() {
    this.thing = new ClassB();
    this.property = 5;
}

var a = new ClassA();

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

b.js

var ClassB = function() {
}

ClassB.prototype.doSomethingLater() {
    util.log(a.property);
}

module.exports = ClassB;

var a = require("./a"); // <------ this is the only necessary change
Run Code Online (Sandbox Code Playgroud)


Nic*_*ich 14

解决方案是在需要任何其他控制器之前"转发声明"您的导出对象.因此,如果您构建所有模块,那么您将不会遇到任何类似的问题:

// Module exports forward declaration:
module.exports = {

};

// Controllers:
var other_module = require('./other_module');

// Functions:
var foo = function () {

};

// Module exports injects:
module.exports.foo = foo;
Run Code Online (Sandbox Code Playgroud)

  • 实际上,这导致我只使用`exports.foo = function(){...}`代替.绝对是诀窍.谢谢! (2认同)
  • 我不确定你在这里提出什么建议。默认情况下 `module.exports` 已经是一个普通对象,因此您的“前向声明”行是多余的。 (2认同)

Fat*_*tie 10

请注意,卡米洛给出了完全正确的答案:

// DO NOT DO THIS
// module.exports = {
//     tell, devTell, devTellPermanentLog,
//     updateAdmin
// }

// DO THIS:
exports.tell = tell
exports.devTell = devTell
exports.devTellPermanentLog = devTellPermanentLog
exports.updateAdmin = updateAdmin
Run Code Online (Sandbox Code Playgroud)

令人难以置信的是,对于 Node.js 中关于循环的数百字数千字来说,这就是整个解决方案。

这里的所有都是它的。

谢谢你,卡米洛。


如果您没有时间重构,这里有一个值得了解的快速解决方法:

如果您只需要立即运行一些东西,一个非常简单的“快速解决方案”是:

通常你会require在文件的顶部...

var script = require('./script')
function stuff() {
      script.farfunction()
}
Run Code Online (Sandbox Code Playgroud)

相反,只需“在函数中”要求它

function stuff() {
      var _script = require('./script')
      _script.farfunction()
}
Run Code Online (Sandbox Code Playgroud)


小智 8

您可以轻松解决这个问题:在使用 module.exports 的模块中需要任何其他内容之前,只需导出您的数据:

类A.js

class ClassA {

    constructor(){
        ClassB.someMethod();
        ClassB.anotherMethod();
    };

    static someMethod () {
        console.log( 'Class A Doing someMethod' );
    };

    static anotherMethod () {
        console.log( 'Class A Doing anotherMethod' );
    };

};

module.exports = ClassA;
var ClassB = require( "./classB.js" );

let classX = new ClassA();
Run Code Online (Sandbox Code Playgroud)

类B.js

class ClassB {

    constructor(){
        ClassA.someMethod();
        ClassA.anotherMethod();
    };

    static someMethod () {
        console.log( 'Class B Doing someMethod' );
    };

    static anotherMethod () {
        console.log( 'Class A Doing anotherMethod' );
    };

};

module.exports = ClassB;
var ClassA = require( "./classA.js" );

let classX = new ClassB();
Run Code Online (Sandbox Code Playgroud)


set*_*tec 7

需要最小改变的解决方案是扩展module.exports而不是覆盖它.

a.js - 使用方法的app入口点和模块来自b.js*

_ = require('underscore'); //underscore provides extend() for shallow extend
b = require('./b'); //module `a` uses module `b`
_.extend(module.exports, {
    do: function () {
        console.log('doing a');
    }
});
b.do();//call `b.do()` which in turn will circularly call `a.do()`
Run Code Online (Sandbox Code Playgroud)

b.js - 使用方法的模块来自a.js

_ = require('underscore');
a = require('./a');

_.extend(module.exports, {
    do: function(){
        console.log('doing b');
        a.do();//Call `b.do()` from `a.do()` when `a` just initalized 
    }
})
Run Code Online (Sandbox Code Playgroud)

它将工作和生产:

doing b
doing a
Run Code Online (Sandbox Code Playgroud)

虽然此代码不起作用:

a.js

b = require('./b');
module.exports = {
    do: function () {
        console.log('doing a');
    }
};
b.do();
Run Code Online (Sandbox Code Playgroud)

b.js

a = require('./a');
module.exports = {
    do: function () {
        console.log('doing b');
    }
};
a.do();
Run Code Online (Sandbox Code Playgroud)

输出:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function
Run Code Online (Sandbox Code Playgroud)

  • 如果你没有`下划线',那么ES6的`Object.assign()`可以完成`_.extend()`在这个答案中所做的同样的工作. (4认同)

joe*_*dle 7

重要的是不要重新分配给module.exports您的对象,因为该对象可能已经被分配给循环中的其他模块!只需在内部分配属性module.exports,其他模块就会看到它们出现。

所以一个简单的解决方案是:

module.exports.firstMember = ___;
module.exports.secondMember = ___;
Run Code Online (Sandbox Code Playgroud)

唯一真正的缺点是需要重复module.exports.多次。


与 lanzz 和 setec 的答案类似,我一直在使用以下模式,感觉更具声明性:

module.exports = Object.assign(module.exports, {
    firstMember: ___,
    secondMember: ___,
});
Run Code Online (Sandbox Code Playgroud)

Object.assign()成员复制到exports已提供给其他模块的对象中。

=分配在逻辑上是多余的,因为它只是设置module.exports为自身,但我使用它是因为它可以帮助我的 IDE (WebStorm) 识别出这firstMember是该模块的属性,因此“转到 -> 声明”(Cmd-B)其他工具将在其他文件中工作。

这种模式不是很漂亮,所以我只在需要解决循环依赖问题时使用它。

它非常适合Reveal 模式,因为您可以轻松地添加和删除对象的导出,特别是在使用 ES6 的属性简写时。

Object.assign(module.exports, {
    firstMember,
    //secondMember,
});
Run Code Online (Sandbox Code Playgroud)


Cam*_*ilo 6

长话短说

只需使用exports.someMember = someMember而不是module.exports = { // new object }.

扩展答案

在阅读了 lanzz 的回复后,我终于可以弄清楚这里发生了什么,所以我将在这个问题上给出我的两分钱,扩展他的答案。

让我们看这个例子:

a.js

console.log("a starting");

console.log("a requires b");
const b = require("./b");
console.log("a gets b =", b);

function functionA() {
  console.log("function a");
}

console.log("a done");
exports.functionA = functionA;
Run Code Online (Sandbox Code Playgroud)

b.js

console.log("b starting");

console.log("b requires a");
const a = require("./a");
console.log("b gets a =", a);

function functionB() {
  console.log("On b, a =", a)
}

console.log("b done");
exports.functionB = functionB;

Run Code Online (Sandbox Code Playgroud)

main.js

const a = require("./a");
const b = require("./b");

b.functionB()
Run Code Online (Sandbox Code Playgroud)

输出

a starting
a requires b
b starting
b requires a
b gets a = {}
b done
a gets b = { functionB: [Function: functionB] }
a done
On b, a = { functionA: [Function: functionA] }
Run Code Online (Sandbox Code Playgroud)

在这里我们可以看到,首先b接收到一个空对象 as a,然后一旦a完全加载,该引用就会通过 更新exports.functionA = functionA。如果您将整个模块替换为另一个对象,通过module.exportsb则将丢失来自 的引用a,因为它将从一开始就指向同一个空对象,而不是指向新对象。

因此,如果您a像这样导出:module.exports = { functionA: functionA },那么输出将是:

a starting
a requires b
b starting
b requires a
b gets a = {}
b done
a gets b = { functionB: [Function: functionB] }
a done
On b, a = {} // same empty object
Run Code Online (Sandbox Code Playgroud)


zev*_*ero 5

只有在你需要的时候,懒惰需要什么呢?所以你的b.js看起来如下

var ClassB = function() {
}
ClassB.prototype.doSomethingLater() {
    var a = require("./a");    //a.js has finished by now
    util.log(a.property);
}
module.exports = ClassB;
Run Code Online (Sandbox Code Playgroud)

当然,最好将所有require语句放在文件的顶部.但次,我在那里原谅自己挑选出来的东西其它不相关的模块.称之为hack,但有时这比引入进一步的依赖,或添加额外的模块或添加新结构(EventEmitter等)更好


Ben*_*dai 5

我见过人们做的另一种方法是在第一行导出并将其保存为本地变量,如下所示:

let self = module.exports = {};

const a = require('./a');

// Exporting the necessary functions
self.func = function() { ... }
Run Code Online (Sandbox Code Playgroud)

我倾向于使用这种方法,您知道它有什么缺点吗?