Come gestire le dipendenze cicliche in Node.js


162

Di recente ho lavorato con nodejs e sto ancora affrontando il sistema dei moduli, quindi mi scuso se questa è una domanda ovvia. Voglio un codice simile al seguente:

a.js (il file principale eseguito con il nodo)

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;

Il mio problema sembra essere che non riesco ad accedere all'istanza di ClassA da un'istanza di ClassB.

Esiste un modo corretto / migliore per strutturare i moduli per ottenere ciò che voglio? Esiste un modo migliore per condividere le variabili tra i moduli?


Ti suggerisco di controllare la separazione delle query, il modello osservabile e quindi quello che i ragazzi del CS chiamano i gestori, che è fondamentalmente un wrapper per il modello osservabile.
dewwwald,

Risposte:


86

Mentre node.js consente requiredipendenze circolari , come hai scoperto può essere piuttosto disordinato e probabilmente stai meglio ristrutturando il codice per non averne bisogno. Forse creare una terza classe che utilizza le altre due per realizzare ciò di cui hai bisogno.


6
+1 Questa è la risposta giusta. Le dipendenze circolari sono odore di codice. Se A e B sono sempre usati insieme sono effettivamente un singolo modulo, quindi uniscili. O trova un modo per spezzare la dipendenza; forse è un modello composito.
James,

94
Non sempre. nei modelli di database, ad esempio, se ho i modelli A e B, nel modello AI potrebbe essere necessario fare riferimento al modello B (ad es. per unire le operazioni) e viceversa. Pertanto, esportare diverse proprietà A e B (quelle che non dipendono da altri moduli) prima di utilizzare la funzione "richiedono" potrebbe essere una risposta migliore.
João Bruno Abou Hatem de Liz,

11
Inoltre non vedo dipendenze circolari come odore di codice. Sto sviluppando un sistema in cui ci sono alcuni casi in cui è necessario. Ad esempio, team di modellazione e utenti, in cui gli utenti possono appartenere a molti team. Quindi, non è che qualcosa non vada nella mia modellazione. Ovviamente, potrei riformattare il mio codice per evitare la dipendenza circolare tra le due entità, ma quella non sarebbe la forma più pura del modello di dominio, quindi non lo farò.
Alexandre Martini,

1
Quindi dovrei iniettare la dipendenza quando necessario, è questo che intendi? Usando un terzo per controllare l'interazione tra le due dipendenze con il problema ciclico?
giovannipds,

2
Questo non è disordinato .. qualcuno potrebbe voler frenare un file per evitare un libro di codice in un singolo file. Come suggerisce il nodo, dovresti aggiungere un exports = {}all'inizio del tuo codice e poi exports = yourDataalla fine del tuo codice. Con questa pratica eviterai quasi tutti gli errori delle dipendenze circolari.
sacerdote

178

Prova a impostare le proprietà module.exports, invece di sostituirlo completamente. Ad esempio, module.exports.instance = new ClassA()in a.js, module.exports.ClassB = ClassBin b.js. Quando si effettuano dipendenze del modulo circolare, il modulo richiedente otterrà un riferimento a un incompleto module.exportsdal modulo richiesto, sul quale è possibile aggiungere altre proprietà su quest'ultimo, ma quando si imposta l'intero module.exports, si crea effettivamente un nuovo oggetto che il modulo richiedente non ha modo di accedere.


6
Questo potrebbe essere tutto vero, ma direi comunque di evitare dipendenze circolari. Prendere accordi speciali per gestire i moduli con suoni caricati in modo incompleto come se crea un problema futuro che non si desidera avere. Questa risposta prescrive una soluzione su come gestire i moduli caricati in modo incompleto ... Non credo sia una buona idea.
Alexander Mills,

1
Come inseriresti un costruttore di classi module.exportssenza sostituirlo completamente, per consentire ad altre classi di "costruire" un'istanza della classe?
Tim Visée,

1
Non penso che tu possa. I moduli che hanno già importato il tuo modulo non saranno in grado di vedere quel cambiamento
lanzz

52

[EDIT] non è il 2015 e la maggior parte delle biblioteche (ad esempio express) ha effettuato aggiornamenti con schemi migliori, quindi le dipendenze circolari non sono più necessarie. Consiglio semplicemente di non usarli .


So che sto cercando una vecchia risposta qui ... Il problema qui è che module.exports è definito dopo aver richiesto ClassB. (che mostra il link di JohnnyHK) Le dipendenze circolari funzionano perfettamente in Node, sono solo definite in modo sincrono. Se utilizzati correttamente, in realtà risolvono molti problemi di nodo comuni (come l'accesso a express.js appda altri file)

Assicurati solo che le tue esportazioni necessarie siano definite prima di richiedere un file con una dipendenza circolare.

Questo si romperà:

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

module.exports = ClassA;

Questo funzionerà:

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

Uso sempre questo schema per accedere a express.js appin altri file:

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

2
grazie per aver condiviso il modello e poi per aver ulteriormente condiviso il modo in cui usi questo modello durante l'esportazioneapp = express()
user566245

34

A volte è davvero artificiale introdurre una terza classe (come consiglia JohnnyHK), quindi oltre a Ianzz: se si desidera sostituire module.exports, ad esempio se si sta creando una classe (come il file b.js in l'esempio sopra), anche questo è possibile, assicurati solo che nel file che sta avviando la circolare richieda, l'istruzione 'module.exports = ...' si verifica prima dell'istruzione request.

a.js (il file principale eseguito con il nodo)

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

grazie, non mi ero mai reso conto che module.exports aveva un effetto sulle dipendenze circolari.
Laurent Perrin,

questo è particolarmente utile con i modelli Mongoose (MongoDB); mi aiuta a risolvere un problema quando il modello BlogPost ha una matrice con riferimenti a commenti e ogni modello di commento ha riferimento a BlogPost.
Oleg Zarevennyi,

14

La soluzione è "dichiarare in avanti" l'oggetto export prima di richiedere qualsiasi altro controller. Quindi se strutturi tutti i tuoi moduli in questo modo e non incontrerai problemi del genere:

// 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
In realtà, questo mi ha portato a usare semplicemente exports.foo = function() {...}invece. Sicuramente ha fatto il trucco. Grazie!
zanona,

Non sono sicuro di ciò che stai proponendo qui. module.exportsè già un semplice oggetto per impostazione predefinita, quindi la riga "dichiarazione anticipata" è ridondante.
ZachB

7

Una soluzione che richiede un cambiamento minimo si sta estendendo module.exportsinvece di sovrascriverla.

a.js - punto di ingresso dell'app e modulo che utilizzano il metodo do da 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 - modulo che usa il metodo do da 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 
    }
})

Funzionerà e produrrà:

doing b
doing a

Mentre questo codice non funzionerà:

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

Produzione:

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

4
Se non lo hai underscore, allora ES6 Object.assign()può fare lo stesso lavoro che _.extend()sta facendo in questa risposta.
joeytwiddle,

5

Che dire di pigro che richiede solo quando è necessario? Quindi il tuo b.js si presenta come segue

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

Naturalmente è buona norma mettere tutte le istruzioni richieste in cima al file. Ma ci sono occasioni in cui mi perdono per aver scelto qualcosa da un modulo altrimenti non correlato. Chiamalo un hack, ma a volte è meglio che introdurre un'ulteriore dipendenza, o aggiungere un modulo aggiuntivo o aggiungere nuove strutture (EventEmitter, ecc.)


E a volte è fondamentale quando si tratta di una struttura di dati ad albero con oggetti figlio mantenendo riferimenti a un genitore. Grazie per il consiglio.
Robert Oschler,

5

Un altro metodo che ho visto fare è esportare in prima linea e salvarlo come variabile locale come questa:

let self = module.exports = {};

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

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

Tendo ad usare questo metodo, conosci qualche svantaggio?


puoi piuttosto fare module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal,

4

Puoi risolverlo facilmente: esporta i tuoi dati prima di richiedere qualsiasi altra cosa nei moduli in cui usi 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();

3

Simile alle risposte di lanzz e setect, ho usato il seguente schema:

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

Le Object.assign()copie dei membri nella exportsoggetto che è già stato dato agli altri moduli.

L' =assegnazione è logicamente ridondante, poiché si sta semplicemente impostando module.exportssu se stessa, ma la sto usando perché aiuta il mio IDE (WebStorm) a riconoscere che firstMemberè una proprietà di questo modulo, quindi "Vai a -> Dichiarazione" (Cmd-B) e altri strumenti funzioneranno da altri file.

Questo modello non è molto carino, quindi lo uso solo quando è necessario risolvere un problema di dipendenza ciclica.


2

Ecco una rapida soluzione che ho trovato piena.

Nel file "a.js"

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

Nel file 'b.js' scrivi quanto segue

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

In questo modo alla successiva iterazione delle classi del ciclo di eventi verrà definita correttamente e quelle istruzioni necessarie funzioneranno come previsto.


1

In realtà ho finito per richiedere la mia dipendenza con

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

non carino, ma funziona. È più comprensibile e onesto che cambiare b.js (ad esempio solo aumentando modules.export), che altrimenti è perfetto così com'è.


Di tutte le soluzioni in questa pagina, questa è l'unica che ha risolto il mio problema. Ho provato ciascuno a turno.
Joe Lapp,

0

Un modo per evitarlo è di non richiedere un file nell'altro semplicemente passarlo come argomento a una funzione di qualsiasi cosa sia necessaria in un altro file. In questo modo la dipendenza circolare non sorgerà mai.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.