Jak radzić sobie z cyklicznymi zależnościami w Node.js.

162

Ostatnio pracowałem z nodejs i wciąż mam do czynienia z systemem modułów, więc przepraszam, jeśli jest to oczywiste pytanie. Chcę kod mniej więcej taki jak poniżej:

a.js (główny plik uruchamiany z węzłem)

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;

Wydaje się, że moim problemem jest to, że nie mogę uzyskać dostępu do wystąpienia ClassA z poziomu wystąpienia ClassB.

Czy istnieje poprawny / lepszy sposób strukturyzacji modułów, aby osiągnąć to, czego chcę? Czy istnieje lepszy sposób udostępniania zmiennych między modułami?

Runcible
źródło
Proponuję przyjrzeć się separacji zapytań poleceń, obserwowalnym wzorcom, a następnie temu, co faceci z CS nazywają menedżerami - co jest w zasadzie opakowaniem dla obserwowalnego wzorca.
dewwwald

Odpowiedzi:

86

Chociaż node.js zezwala na requirezależności cykliczne , jak zauważyłeś, może być dość niechlujny i prawdopodobnie lepiej będzie zreorganizować kod, aby go nie potrzebował. Może stwórz trzecią klasę, która wykorzysta pozostałe dwie, aby osiągnąć to, czego potrzebujesz.

JohnnyHK
źródło
6
+1 To jest właściwa odpowiedź. Zależności cykliczne to zapach kodu. Jeśli A i B są zawsze używane razem, w rzeczywistości stanowią pojedynczy moduł, więc połącz je. Albo znajdź sposób na zerwanie z zależnością; może to złożony wzór.
James
94
Nie zawsze. w modelach baz danych, na przykład, jeśli mam model A i B, w modelu AI może chcieć odwołać się do modelu B (np. aby połączyć operacje) i odwrotnie. Dlatego wyeksportuj kilka właściwości A i B (tych, które nie zależą od innych modułów) przed użyciem funkcji „require” może być lepszą odpowiedzią.
João Bruno Abou Hatem de Liz
11
Nie widzę też zależności cyklicznych jako zapachu kodu. Tworzę system, w którym jest kilka przypadków, w których jest to potrzebne. Na przykład modelowanie zespołów i użytkowników, gdzie użytkownicy mogą należeć do wielu zespołów. Więc nie chodzi o to, że coś jest nie tak z moim modelowaniem. Oczywiście mógłbym dokonać refaktoryzacji kodu, aby uniknąć cyklicznej zależności między dwiema jednostkami, ale nie byłaby to najczystsza forma modelu domeny, więc nie zrobię tego.
Alexandre Martini,
1
W takim razie powinienem wstrzyknąć zależność w razie potrzeby, czy to masz na myśli? Używając trzeciej do kontrolowania interakcji między dwiema zależnościami z problemem cyklicznym?
giovannipds
2
To nie jest bałagan… ktoś może chcieć złamać plik, aby uniknąć książki z kodem, np. Pojedynczego pliku. Jak sugeruje węzeł, powinieneś dodać znak exports = {}na górze swojego kodu, a następnie exports = yourDatana końcu kodu. Dzięki tej praktyce unikniesz prawie wszystkich błędów wynikających z zależności cyklicznych.
ksiądz
178

Spróbuj włączyć właściwości module.exports, zamiast całkowicie je zastępować. Np. module.exports.instance = new ClassA()W a.js, module.exports.ClassB = ClassBw b.js. Kiedy tworzysz cykliczne zależności modułów, module.exportsżądany moduł otrzyma odwołanie do niekompletnego z wymaganego modułu, do którego możesz dodać inne właściwości, ale kiedy ustawisz całość module.exports, w rzeczywistości utworzysz nowy obiekt, którego żądany moduł nie ma sposób dostępu.

lanzz
źródło
6
To może być prawda, ale powiedziałbym, że nadal unikaj zależności cyklicznych. Wykonywanie specjalnych przygotowań do obsługi modułów, które mają niecałkowicie załadowane dźwięki, stwarzają w przyszłości problem, którego nie chcesz mieć. Ta odpowiedź podpowiada rozwiązanie, jak radzić sobie z niecałkowicie załadowanymi modułami ... Nie sądzę, żeby to był dobry pomysł.
Alexander Mills
1
W jaki sposób umieściłbyś konstruktora klasy module.exportsbez całkowitego zastępowania go, aby umożliwić innym klasom „skonstruowanie” instancji klasy?
Tim Visée
1
Myślę, że nie możesz. Moduły, które już zaimportowały Twój moduł, nie będą w stanie zobaczyć tej zmiany
lanzz
52

[EDYTUJ] to nie rok 2015 i większość bibliotek (np. Ekspresowych) dokonało aktualizacji z lepszymi wzorcami, więc zależności cykliczne nie są już potrzebne. Zalecam po prostu ich nie używać .


Wiem, że wykopuję tutaj starą odpowiedź ... Problem polega na tym, że module.exports jest definiowany po tym, jak potrzebujesz ClassB. (co pokazuje link JohnnyHK) Zależności cykliczne działają świetnie w Node, są po prostu definiowane synchronicznie. Prawidłowo używane rozwiązują wiele typowych problemów z węzłami (np. Uzyskiwanie dostępu do express.js appz innych plików)

Po prostu upewnij się, że niezbędne eksporty są zdefiniowane, zanim będziesz potrzebować pliku z zależnością cykliczną.

To się zepsuje:

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

module.exports = ClassA;

To zadziała:

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

Cały czas używam tego wzorca, aby uzyskać dostęp do express.js appw innych plikach:

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app
Will Stern
źródło
2
dziękuję za udostępnienie wzoru, a następnie dalsze dzielenie się tym, jak często używasz tego wzoru podczas eksportowaniaapp = express()
user566245
34

Czasami wprowadzenie trzeciej klasy jest naprawdę sztuczne (jak radzi JohnnyHK), więc oprócz Ianzza: Jeśli chcesz zamienić module.exports, na przykład jeśli tworzysz klasę (jak plik b.js w powyższy przykład), jest to również możliwe, po prostu upewnij się, że w pliku, który rozpoczyna cykliczne żądanie, instrukcja „module.exports = ...” występuje przed instrukcją require.

a.js (główny plik uruchamiany z węzłem)

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
Coen
źródło
dzięki coen, nigdy nie zdawałem sobie sprawy, że module.exports ma wpływ na zależności cykliczne.
Laurent Perrin
jest to szczególnie przydatne w przypadku modeli Mongoose (MongoDB); pomaga mi rozwiązać problem, gdy model BlogPost ma tablicę z odniesieniami do komentarzy, a każdy model komentarza ma odniesienie do BlogPost.
Oleg Zarevennyi
14

Rozwiązaniem jest „zadeklarowanie dalej” obiektu eksportu przed zażądaniem jakiegokolwiek innego kontrolera. Więc jeśli uporządkujesz wszystkie swoje moduły w ten sposób i nie napotkasz żadnych takich problemów:

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

};

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

// Functions:
var foo = function () {

};

// Module exports injects:
module.exports.foo = foo;
Nicolas Gramlich
źródło
3
Właściwie to sprawiło, że exports.foo = function() {...}zamiast tego po prostu użyłem. Zdecydowanie załatwił sprawę. Dzięki!
zanona
Nie jestem pewien, co tu proponujesz. module.exportsjest już zwykłym obiektem, więc twoja linia "deklaracji do przodu" jest zbędna.
ZachB
7

Rozwiązaniem wymagającym minimalnej zmiany jest rozszerzenie, module.exportsa nie nadpisanie.

a.js - punkt wejścia aplikacji i moduł używający metody do z 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()`

b.js - moduł używający metody do z 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 
    }
})

Będzie działać i produkować:

doing b
doing a

Chociaż ten kod nie zadziała:

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

Wynik:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function
setec
źródło
4
Jeśli nie masz underscore, ES6 Object.assign()mogą wykonać tę samą pracę, _.extend()co w tej odpowiedzi.
joeytwiddle
5

A co z leniwym wymaganiem tylko wtedy, gdy tego potrzebujesz? Więc twój b.js wygląda następująco

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

Oczywiście dobrą praktyką jest umieszczanie wszystkich wymaganych instrukcji na wierzchu pliku. Ale sytuacje, w których wybaczam sobie wybieranie czegoś z niepowiązanego modułu. Nazwij to hackiem, ale czasami jest to lepsze niż wprowadzenie dalszej zależności lub dodanie dodatkowego modułu lub dodanie nowych struktur (EventEmitter itp.)

zevero
źródło
Czasami jest to krytyczne, gdy mamy do czynienia z drzewiastą strukturą danych, w której obiekty potomne zachowują odniesienia do rodzica. Dzięki za wskazówkę.
Robert Oschler,
5

Inną metodą, którą widziałem, jest eksportowanie w pierwszym wierszu i zapisywanie jej jako zmiennej lokalnej w następujący sposób:

let self = module.exports = {};

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

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

Zwykle używam tej metody, czy znasz jakieś jej wady?

Bence Gedai
źródło
możesz raczej zrobić module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal
4

Możesz to łatwo rozwiązać: po prostu wyeksportuj swoje dane, zanim będziesz potrzebować czegokolwiek innego w modułach, w których używasz module.exports:

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();
Giuseppe Canale
źródło
3

Podobnie jak w przypadku odpowiedzi Lanzza i Setecta, korzystam z następującego wzoru:

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

W Object.assign()kopiuje członków do exportsobiektu, który już został dany do innych modułów.

=Zadaniem jest logicznie zbędny, ponieważ jest po prostu ustawienie module.exportsna sobie, ale używam go, ponieważ pomaga moja IDE (WebStorm) uznać, że firstMemberjest własnością tego modułu, więc „przejść do -> Deklaracja” (Cmd-B) a inne narzędzia będą działać z innych plików.

Ten wzorzec nie jest zbyt ładny, więc używam go tylko wtedy, gdy trzeba rozwiązać problem cyklicznej zależności.

joeytwiddle
źródło
2

Oto szybkie obejście, które znalazłem w pełni.

W pliku „a.js”

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

W pliku „b.js” zapisz co następuje

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

W ten sposób w następnej iteracji klasy pętli zdarzeń zostaną poprawnie zdefiniowane, a te instrukcje będą działać zgodnie z oczekiwaniami.

Melik Karapetyan
źródło
1

Właściwie skończyło się na tym, że wymagałem mojej zależności od

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

nie ładne, ale działa. Jest to bardziej zrozumiałe i uczciwe niż zmiana b.js (na przykład tylko rozszerzenie modułów.export), która poza tym jest doskonała.

zevero
źródło
Ze wszystkich rozwiązań na tej stronie, jest to jedyne, które rozwiązało mój problem. Próbowałem po kolei.
Joe Lapp
0

Jednym ze sposobów uniknięcia tego jest nie wymaganie jednego pliku w innym, po prostu przekazanie go jako argumentu funkcji, czego potrzebujesz w innym pliku. W ten sposób nigdy nie powstanie zależność cykliczna.

sagar saini
źródło