Umgang mit zyklischen Abhängigkeiten in Node.js.


162

Ich habe in letzter Zeit mit NodeJS gearbeitet und mich immer noch mit dem Modulsystem auseinandergesetzt. Ich entschuldige mich, wenn dies eine offensichtliche Frage ist. Ich möchte Code ungefähr wie folgt:

a.js (die Hauptdatei, die mit dem Knoten ausgeführt wird)

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

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

var a = new ClassA();

module.exports = a;

b.js.

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

var ClassB = function() {
}

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

module.exports = ClassB;

Mein Problem scheint zu sein, dass ich innerhalb einer Instanz von ClassB nicht auf die Instanz von ClassA zugreifen kann.

Gibt es eine korrekte / bessere Möglichkeit, Module zu strukturieren, um das zu erreichen, was ich will? Gibt es eine bessere Möglichkeit, Variablen zwischen Modulen auszutauschen?


Ich schlage vor, Sie schauen sich die Abfragetrennung, das beobachtbare Muster und dann das an, was die CS-Leute Manager nennen - was im Grunde ein Wrapper für das beobachtbare Muster ist.
Dewwwald

Antworten:


86

Während node.js zirkuläre requireAbhängigkeiten zulässt , kann es, wie Sie festgestellt haben, ziemlich chaotisch sein und Sie sollten Ihren Code wahrscheinlich besser umstrukturieren, um ihn nicht zu benötigen. Erstellen Sie möglicherweise eine dritte Klasse, die die beiden anderen verwendet, um das zu erreichen, was Sie benötigen.


6
+1 Dies ist die richtige Antwort. Zirkuläre Abhängigkeiten sind Codegeruch. Wenn A und B immer zusammen verwendet werden, handelt es sich effektiv um ein einzelnes Modul. Führen Sie sie daher zusammen. Oder einen Weg finden, die Abhängigkeit zu brechen; Vielleicht ist es ein zusammengesetztes Muster.
James

94
Nicht immer. Wenn ich beispielsweise in Datenbankmodellen Modell A und B habe, möchte ich in Modell AI möglicherweise auf Modell B verweisen (z. B. um Operationen zu verbinden) und umgekehrt. Exportieren Sie daher mehrere A- und B-Eigenschaften (diejenigen, die nicht von anderen Modulen abhängen), bevor Sie die Funktion "require" verwenden. Dies ist möglicherweise eine bessere Antwort.
João Bruno Abou Hatem de Liz

11
Ich sehe auch keine zirkulären Abhängigkeiten als Codegeruch. Ich entwickle ein System, in dem es einige Fälle gibt, in denen es benötigt wird. Zum Beispiel Modellierung von Teams und Benutzern, bei denen Benutzer zu vielen Teams gehören können. Es ist also nicht so, dass etwas mit meiner Modellierung nicht stimmt. Natürlich könnte ich meinen Code umgestalten, um die zirkuläre Abhängigkeit zwischen den beiden Entitäten zu vermeiden, aber das wäre nicht die reinste Form des Domänenmodells, also werde ich das nicht tun.
Alexandre Martini

1
Sollte ich dann die Abhängigkeit bei Bedarf injizieren, meinen Sie das? Verwenden Sie ein Drittel, um die Interaktion zwischen den beiden Abhängigkeiten mit dem zyklischen Problem zu steuern?
Giovannipds

2
Dies ist nicht unordentlich. Möglicherweise möchte jemand eine Datei bremsen, um ein Codebuch zu vermeiden, z. B. eine einzelne Datei. Wie der Knoten vorschlägt, sollten Sie exports = {}oben in Ihrem Code und dann exports = yourDataam Ende Ihres Codes ein hinzufügen . Mit dieser Vorgehensweise vermeiden Sie fast alle Fehler aus zirkulären Abhängigkeiten.
Prieston

177

Versuchen Sie, Eigenschaften festzulegen module.exports, anstatt sie vollständig zu ersetzen. ZB module.exports.instance = new ClassA()in a.js, module.exports.ClassB = ClassBin b.js. Wenn Sie kreisförmige Modulabhängigkeiten erstellen, erhält das erforderliche Modul einen Verweis auf ein unvollständiges module.exportsModul aus dem erforderlichen Modul, zu dem Sie andere Eigenschaften hinzufügen können. Wenn Sie jedoch das gesamte module.exportsModul festlegen , erstellen Sie tatsächlich ein neues Objekt, das das erforderliche Modul nicht hat Weg zum Zugang.


6
Dies mag alles wahr sein, aber ich würde sagen, vermeiden Sie immer noch zirkuläre Abhängigkeiten. Wenn Sie spezielle Vorkehrungen treffen, um mit Modulen umzugehen, deren Klänge unvollständig geladen sind, entsteht ein zukünftiges Problem, das Sie nicht haben möchten. Diese Antwort schreibt eine Lösung für den Umgang mit unvollständig geladenen Modulen vor ... Ich halte das nicht für eine gute Idee.
Alexander Mills

1
Wie würden Sie einen Klassenkonstruktor einfügen, module.exportsohne ihn vollständig zu ersetzen, damit andere Klassen eine Instanz der Klasse "konstruieren" können?
Tim Visée

1
Ich glaube nicht, dass du kannst. Module, die Ihr Modul bereits importiert haben, können diese Änderung nicht sehen
lanzz

52

[BEARBEITEN] Es ist nicht 2015 und die meisten Bibliotheken (dh Express) haben Aktualisierungen mit besseren Mustern vorgenommen, sodass zirkuläre Abhängigkeiten nicht mehr erforderlich sind. Ich empfehle sie einfach nicht zu benutzen .


Ich weiß, dass ich hier eine alte Antwort ausgrabe ... Das Problem hier ist, dass module.exports definiert wird, nachdem Sie ClassB benötigen. (der Link von JohnnyHK zeigt) Zirkuläre Abhängigkeiten funktionieren in Node hervorragend, sie werden nur synchron definiert. Bei richtiger Verwendung lösen sie tatsächlich viele häufig auftretende Knotenprobleme (z. B. den Zugriff auf express.js appaus anderen Dateien).

Stellen Sie einfach sicher, dass Ihre erforderlichen Exporte definiert sind, bevor Sie eine Datei mit einer zirkulären Abhängigkeit benötigen.

Dies wird brechen:

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

module.exports = ClassA;

Das wird funktionieren:

var ClassA = module.exports = function(){};
var ClassB = require('classB');

Ich benutze dieses Muster ständig für den Zugriff auf die appDatei express.js in anderen Dateien:

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

2
Vielen Dank, dass Sie das Muster geteilt haben und dann weiter teilen, wie Sie dieses Muster beim Exportieren häufig verwendenapp = express()
user566245

34

Manchmal ist es wirklich künstlich, eine dritte Klasse einzuführen (wie JohnnyHK empfiehlt), also zusätzlich zu Ianzz: Wenn Sie die module.exports ersetzen möchten, zum Beispiel, wenn Sie eine Klasse erstellen (wie die Datei b.js in Im obigen Beispiel ist dies ebenfalls möglich. Stellen Sie einfach sicher, dass in der Datei, in der das Rundschreiben erforderlich ist, die Anweisung 'module.exports = ...' vor der Anweisung require ausgeführt wird.

a.js (die Hauptdatei, die mit dem Knoten ausgeführt wird)

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

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

var a = new ClassA();

module.exports = a;

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

danke coen, ich hatte nie bemerkt, dass module.exports einen Einfluss auf zirkuläre Abhängigkeiten hat.
Laurent Perrin

Dies ist besonders nützlich bei Mongoose (MongoDB) -Modellen. hilft mir, ein Problem zu beheben, wenn das BlogPost-Modell ein Array mit Verweisen auf Kommentare enthält und jedes Kommentarmodell einen Verweis auf BlogPost enthält.
Oleg Zarevennyi

14

Die Lösung besteht darin, Ihr Exportobjekt weiterzuleiten, bevor Sie einen anderen Controller benötigen. Wenn Sie also alle Ihre Module so strukturieren und auf solche Probleme nicht stoßen:

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

};

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

// Functions:
var foo = function () {

};

// Module exports injects:
module.exports.foo = foo;

3
Eigentlich hat mich das dazu gebracht, exports.foo = function() {...}stattdessen einfach zu verwenden . Auf jeden Fall den Trick gemacht. Vielen Dank!
Zanona

Ich bin mir nicht sicher, was Sie hier vorschlagen. module.exportsist standardmäßig bereits ein einfaches Objekt, daher ist Ihre Zeile "Vorwärtsdeklaration" redundant.
ZachB

7

Eine Lösung, die nur minimale Änderungen erfordert, wird erweitert, module.exportsanstatt sie zu überschreiben.

a.js - App-Einstiegspunkt und Modul, die die Methode do von b.js * verwenden

_ = 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()`

b.js - Modul, das die Methode do von a.js verwendet

_ = 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 
    }
})

Es wird funktionieren und produzieren:

doing b
doing a

Während dieser Code nicht funktioniert:

a.js

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

b.js.

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

Ausgabe:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function

4
Wenn dies nicht der underscoreFall ist, Object.assign()können ES6 die gleiche Arbeit leisten wie _.extend()in dieser Antwort.
Joeytwiddle

5

Was ist mit faulen Anforderungen nur, wenn Sie müssen? Ihr b.js sieht also wie folgt aus

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

Natürlich ist es empfehlenswert, alle erforderlichen Anweisungen über die Datei zu setzen. Aber es gibt Fälle, in denen ich mir vergebe, etwas aus einem ansonsten nicht verwandten Modul herausgesucht zu haben. Nennen wir es einen Hack, aber manchmal ist dies besser, als eine weitere Abhängigkeit einzuführen oder ein zusätzliches Modul hinzuzufügen oder neue Strukturen hinzuzufügen (EventEmitter usw.)


Und manchmal ist es wichtig, wenn es sich um eine Baumdatenstruktur handelt, bei der untergeordnete Objekte Verweise auf ein übergeordnetes Objekt beibehalten. Danke für den Tipp.
Robert Oschler

5

Eine andere Methode, die ich gesehen habe, ist das Exportieren in der ersten Zeile und das Speichern als lokale Variable wie folgt:

let self = module.exports = {};

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

// Exporting the necessary functions
self.func = function() { ... }

Ich neige dazu, diese Methode zu verwenden. Kennen Sie die Nachteile?


Sie können eher tun module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal

4

Sie können dies einfach lösen: Exportieren Sie einfach Ihre Daten, bevor Sie etwas anderes in Modulen benötigen, in denen Sie module.exports verwenden:

classA.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();

classB.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();

3

Ähnlich wie bei den Antworten von Lanzz und Setect habe ich das folgende Muster verwendet:

module.exports = Object.assign(module.exports, {
    firstMember: ___,
    secondMember: ___,
});

Das Object.assign()kopiert die Mitglieder in das exportsObjekt, das bereits anderen Modulen übergeben wurde.

Die =Zuweisung ist logisch redundant, da sie nur module.exportsauf sich selbst festgelegt wird. Ich verwende sie jedoch, weil sie meiner IDE (WebStorm) hilft, zu erkennen, dass dies firstMembereine Eigenschaft dieses Moduls ist. "Gehe zu -> Deklaration" (Cmd-B) und andere Werkzeuge funktionieren mit anderen Dateien.

Dieses Muster ist nicht sehr hübsch, daher verwende ich es nur, wenn ein zyklisches Abhängigkeitsproblem behoben werden muss.


2

Hier ist eine schnelle Problemumgehung, die ich als vollwertig empfunden habe.

In der Datei 'a.js'

let B;
class A{
  constructor(){
    process.nextTick(()=>{
      B = require('./b')
    })
  } 
}
module.exports = new A();

Schreiben Sie in die Datei 'b.js' Folgendes

let A;
class B{
  constructor(){
    process.nextTick(()=>{
      A = require('./a')
    })
  } 
}
module.exports = new B();

Auf diese Weise werden bei der nächsten Iteration der Ereignisschleifenklassen korrekt definiert und die erforderlichen Anweisungen funktionieren wie erwartet.


1

Eigentlich brauchte ich meine Abhängigkeit von

 var a = null;
 process.nextTick(()=>a=require("./a")); //Circular reference!

nicht schön, aber es funktioniert. Es ist verständlicher und ehrlicher als das Ändern von b.js (zum Beispiel nur das Erweitern von modules.export), was ansonsten so wie es ist perfekt ist.


Von allen Lösungen auf dieser Seite ist dies die einzige, die mein Problem gelöst hat. Ich habe es nacheinander versucht.
Joe Lapp

0

Eine Möglichkeit, dies zu vermeiden, besteht darin, keine Datei in einer anderen zu benötigen. Übergeben Sie sie einfach als Argument an eine Funktion, was auch immer Sie in einer anderen Datei benötigen. Auf diese Weise entsteht niemals eine zirkuläre Abhängigkeit.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.