Comment gérer les dépendances cycliques dans Node.js

162

J'ai travaillé avec nodejs ces derniers temps et je me suis toujours familiarisé avec le système de modules, alors excuses si c'est une question évidente. Je veux du code à peu près comme ci-dessous:

a.js (le fichier principal exécuté avec node)

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;

Mon problème semble être que je ne peux pas accéder à l'instance de ClassA à partir d'une instance de ClassB.

Existe-t-il un moyen correct / meilleur de structurer les modules pour obtenir ce que je veux? Existe-t-il une meilleure façon de partager des variables entre les modules?

Runcible
la source
Je vous suggère de regarder pour commander la séparation des requêtes, le modèle observable et ensuite ce que les gars de CS appellent les gestionnaires - qui est essentiellement un wrapper pour le modèle observable.
dewwwald

Réponses:

86

Bien que node.js autorise les requiredépendances circulaires , comme vous l'avez trouvé, cela peut être assez compliqué et il vaut probablement mieux restructurer votre code pour ne pas en avoir besoin. Créez peut-être une troisième classe qui utilise les deux autres pour accomplir ce dont vous avez besoin.

JohnnyHK
la source
6
+1 C'est la bonne réponse. Les dépendances circulaires sont une odeur de code. Si A et B sont toujours utilisés ensemble, ils forment en fait un seul module, alors fusionnez-les. Ou trouvez un moyen de briser la dépendance; peut-être que c'est un motif composite.
James
94
Pas toujours. dans les modèles de base de données, par exemple, si j'ai les modèles A et B, dans le modèle AI peut vouloir référencer le modèle B (par exemple pour joindre des opérations), et vice-versa. Par conséquent, exporter plusieurs propriétés A et B (celles qui ne dépendent pas d'autres modules) avant d'utiliser la fonction «exiger» peut être une meilleure réponse.
João Bruno Abou Hatem de Liz
11
Je ne vois pas non plus les dépendances circulaires comme une odeur de code. Je développe un système où il y a quelques cas où il est nécessaire. Par exemple, les équipes de modélisation et les utilisateurs, où les utilisateurs peuvent appartenir à de nombreuses équipes. Donc, ce n'est pas que quelque chose ne va pas avec ma modélisation. Évidemment, je pourrais refactoriser mon code pour éviter la dépendance circulaire entre les deux entités, mais ce ne serait pas la forme la plus pure du modèle de domaine, donc je ne le ferai pas.
Alexandre Martini
1
Puis-je injecter la dépendance en cas de besoin, c'est ce que tu veux dire? Utiliser un troisième pour contrôler l'interaction entre les deux dépendances avec le problème cyclique?
giovannipds
2
Ce n'est pas compliqué ... quelqu'un peut vouloir freiner un fichier pour éviter un livre de code dans un seul fichier. Comme le suggère node, vous devez ajouter un exports = {}en haut de votre code, puis exports = yourDataà la fin de votre code. Avec cette pratique, vous éviterez presque toutes les erreurs des dépendances circulaires.
Prieston
178

Essayez de définir les propriétés au module.exportslieu de les remplacer complètement. Par exemple, module.exports.instance = new ClassA()dans a.js, module.exports.ClassB = ClassBdans b.js. Lorsque vous créez des dépendances de modules circulaires, le module requérant obtiendra une référence à un module.exportsmodule incomplet du module requis, sur lequel vous pouvez ajouter d'autres propriétés ultérieurement, mais lorsque vous définissez l'ensemble module.exports, vous créez en fait un nouvel objet sur lequel le module requérant n'a pas moyen d'accès.

Lanzz
la source
6
Tout cela est peut-être vrai, mais je dirais toujours d'éviter les dépendances circulaires. Faire des arrangements spéciaux pour traiter les modules qui ont des sons incomplètement chargés comme si cela créera un problème futur que vous ne voudrez pas avoir. Cette réponse prescrit une solution sur la façon de traiter les modules incomplètement chargés ... Je ne pense pas que ce soit une bonne idée.
Alexander Mills
1
Comment placeriez-vous un constructeur de classe module.exportssans le remplacer complètement, pour permettre à d'autres classes de «construire» une instance de la classe?
Tim Visée
1
Je ne pense pas que vous puissiez. Les modules qui ont déjà importé votre module ne pourront pas voir ce changement
lanzz
52

[EDIT] ce n'est pas 2015 et la plupart des bibliothèques (c'est-à-dire express) ont fait des mises à jour avec de meilleurs modèles donc les dépendances circulaires ne sont plus nécessaires. Je recommande simplement de ne pas les utiliser .


Je sais que je déterre une vieille réponse ici ... Le problème ici est que module.exports est défini après que vous ayez besoin de ClassB. (ce que montre le lien de JohnnyHK) Les dépendances circulaires fonctionnent très bien dans Node, elles sont simplement définies de manière synchrone. Lorsqu'ils sont utilisés correctement, ils résolvent en fait de nombreux problèmes de nœuds courants (comme accéder à express.js à apppartir d'autres fichiers)

Assurez-vous simplement que vos exportations nécessaires sont définies avant de demander un fichier avec une dépendance circulaire.

Cela va casser:

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

module.exports = ClassA;

Cela fonctionnera:

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

J'utilise ce modèle tout le temps pour accéder à express.js appdans d'autres fichiers:

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app
Will Stern
la source
2
merci d'avoir partagé le modèle, puis de partager davantage comment vous utilisez couramment ce modèle lors de l'exportationapp = express()
user566245
34

Parfois, il est vraiment artificiel d'introduire une troisième classe (comme le conseille JohnnyHK), donc en plus de Ianzz: Si vous voulez remplacer le module.exports, par exemple si vous créez une classe (comme le fichier b.js dans l'exemple ci-dessus), cela est également possible, assurez-vous simplement que dans le fichier qui démarre la circulaire require, l'instruction 'module.exports = ...' arrive avant l'instruction require.

a.js (le fichier principal exécuté avec node)

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
la source
merci coen, je n'avais jamais réalisé que module.exports avait un effet sur les dépendances circulaires.
Laurent Perrin
ceci est particulièrement utile avec les modèles Mongoose (MongoDB); m'aide à résoudre un problème lorsque le modèle BlogPost a un tableau avec des références aux commentaires et que chaque modèle Comment fait référence au BlogPost.
Oleg Zarevennyi
14

La solution est de «déclarer en avant» votre objet d'exportations avant de requérir un autre contrôleur. Donc, si vous structurez tous vos modules comme ceci et que vous ne rencontrerez aucun problème de ce genre:

// 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
la source
3
En fait, cela m'a conduit à utiliser simplement à la exports.foo = function() {...}place. Vraiment fait l'affaire. Merci!
zanona
Je ne suis pas sûr de ce que vous proposez ici. module.exportsest déjà un objet simple par défaut, donc votre ligne "déclaration avant" est redondante.
ZachB le
7

Une solution qui nécessite un changement minimal consiste à étendre module.exportsau lieu de la remplacer.

a.js - point d'entrée d'application et module utilisant la méthode do from 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 - module qui utilise la méthode do à partir de 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 
    }
})

Il fonctionnera et produira:

doing b
doing a

Bien que ce code ne fonctionnera pas:

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

Production:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function
setec
la source
4
Si vous ne l'avez pas underscore, alors ES6 Object.assign()peut faire le même travail que _.extend()dans cette réponse.
joeytwiddle
5

Qu'en est-il de la paresse exigeant uniquement lorsque vous en avez besoin? Donc votre b.js ressemble à ceci

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

Bien sûr, il est recommandé de placer toutes les instructions require au-dessus du fichier. Mais il y a des occasions où je me pardonne d'avoir choisi quelque chose dans un module autrement sans rapport. Appelez cela un hack, mais parfois c'est mieux que d'introduire une dépendance supplémentaire, ou d'ajouter un module supplémentaire ou d'ajouter de nouvelles structures (EventEmitter, etc.)

zevero
la source
Et parfois, c'est essentiel lorsqu'il s'agit d'une structure de données arborescente avec des objets enfants conservant des références à un parent. Merci pour le conseil.
Robert Oschler
5

Une autre méthode que j'ai vue des gens est d'exporter à la première ligne et de l'enregistrer en tant que variable locale comme celle-ci:

let self = module.exports = {};

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

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

J'ai tendance à utiliser cette méthode, connaissez-vous ses inconvénients?

Bence Gedai
la source
vous pouvez plutôt faire module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal
4

Vous pouvez résoudre ce problème facilement: exportez simplement vos données avant de demander quoi que ce soit d'autre dans les modules où vous utilisez 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
la source
3

Semblable aux réponses de lanzz et setect, j'ai utilisé le modèle suivant:

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

Les Object.assign()copies des membres dans l' exportsobjet qui a déjà été donné à d' autres modules.

L' =affectation est logiquement redondante, car elle se définit simplement module.exports, mais je l'utilise car elle aide mon IDE (WebStorm) à reconnaître quefirstMember c'est une propriété de ce module, donc "Aller à -> Déclaration" (Cmd-B) et d'autres outils fonctionneront à partir d'autres fichiers.

Ce modèle n'est pas très joli, donc je ne l'utilise que lorsqu'un problème de dépendance cyclique doit être résolu.

joeytwiddle
la source
2

Voici une solution de contournement rapide que j'ai trouvée pleinement utilisée.

Dans le fichier 'a.js'

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

Sur le fichier 'b.js', écrivez ce qui suit

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

De cette façon, lors de la prochaine itération, les classes de la boucle d'événements seront définies correctement et les instructions require fonctionneront comme prévu.

Melik Karapetyan
la source
1

En fait, j'ai fini par exiger ma dépendance avec

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

Ce n'est pas beau, mais ça marche. C'est plus compréhensible et honnête que de changer b.js (par exemple en augmentant uniquement les modules.export), qui autrement est parfait tel quel.

zevero
la source
De toutes les solutions sur cette page, c'est la seule qui a résolu mon problème. J'ai essayé chacun à son tour.
Joe Lapp
0

Une façon de l'éviter est de ne pas avoir besoin d'un fichier dans un autre, simplement de le transmettre comme argument à une fonction, ce dont vous avez besoin dans un autre fichier. De cette manière, la dépendance circulaire ne se produira jamais.

sagar saini
la source