Cómo lidiar con dependencias cíclicas en Node.js

162

He estado trabajando con nodejs últimamente y todavía me estoy familiarizando con el sistema de módulos, así que me disculpo si esta es una pregunta obvia. Quiero un código similar al siguiente a continuación:

a.js (el archivo principal se ejecuta con el 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;

Mi problema parece ser que no puedo acceder a la instancia de ClassA desde una instancia de ClassB.

¿Existe una forma correcta / mejor de estructurar módulos para lograr lo que quiero? ¿Hay una mejor manera de compartir variables entre módulos?

Runcible
fuente
Le sugiero que busque la separación de consultas de comandos, el patrón observable y luego lo que los chicos de CS llaman gerentes, que es básicamente un contenedor para el patrón observable.
dewwwald

Respuestas:

86

Si bien node.js sí permite requiredependencias circulares , como ha descubierto, puede ser bastante complicado y probablemente sea mejor reestructurar su código para no necesitarlo. Tal vez cree una tercera clase que use las otras dos para lograr lo que necesita.

JohnnyHK
fuente
66
+1 Esta es la respuesta correcta. Las dependencias circulares son código olor. Si A y B siempre se usan juntos, son efectivamente un solo módulo, así que combínelos. O encuentre una manera de romper la dependencia; tal vez es un patrón compuesto.
James
94
No siempre. en los modelos de bases de datos, por ejemplo, si tengo el modelo A y B, en el modelo AI puede querer hacer referencia al modelo B (por ejemplo, para unir operaciones), y viceversa. Por lo tanto, exportar varias propiedades A y B (las que no dependen de otros módulos) antes de usar la función "requerir" puede ser una mejor respuesta.
João Bruno Abou Hatem de Liz
11
Tampoco veo dependencias circulares como olor a código. Estoy desarrollando un sistema donde hay algunos casos en los que se necesita. Por ejemplo, modelando equipos y usuarios, donde los usuarios pueden pertenecer a muchos equipos. Entonces, no es que algo esté mal con mi modelado. Obviamente, podría refactorizar mi código para evitar la dependencia circular entre las dos entidades, pero esa no sería la forma más pura del modelo de dominio, por lo que no haré eso.
Alexandre Martini
1
Entonces debería inyectar la dependencia cuando sea necesario, ¿es eso lo que quieres decir? ¿Usando un tercero para controlar la interacción entre las dos dependencias con el problema cíclico?
giovannipds
2
Esto no es desordenado ... alguien puede querer romper un archivo para evitar un libro de código ia un archivo único. Como sugiere el nodo, debe agregar un exports = {}en la parte superior de su código y luego exports = yourDataal final de su código. Con esta práctica evitará casi todos los errores de dependencias circulares.
prieston
178

Intente establecer propiedades en module.exportslugar de reemplazarlo por completo. Por ejemplo, module.exports.instance = new ClassA()en a.js, module.exports.ClassB = ClassBen b.js. Cuando realiza dependencias de módulos circulares, el módulo requerido obtendrá una referencia a un incompleto module.exportsdel módulo requerido, al que puede agregar otras propiedades más adelante, pero cuando configura todo module.exports, realmente crea un nuevo objeto que el módulo requerido no tiene manera de acceder.

lanzz
fuente
66
Esto podría ser todo cierto, pero yo diría que aún evite las dependencias circulares. Hacer arreglos especiales para lidiar con los módulos que tienen una carga incompleta suena como que creará un problema futuro que no desea tener. Esta respuesta prescribe una solución para tratar con módulos cargados incompletamente ... No creo que sea una buena idea.
Alexander Mills
1
¿Cómo colocaría un constructor de clases module.exportssin reemplazarlo por completo, para permitir que otras clases 'construyan' una instancia de la clase?
Tim Visée el
1
No creo que puedas. Los módulos que ya han importado su módulo no podrán ver ese cambio
Lanzz
52

[EDITAR] no es 2015 y la mayoría de las bibliotecas (es decir, express) han realizado actualizaciones con mejores patrones, por lo que las dependencias circulares ya no son necesarias. Recomiendo simplemente no usarlos .


Sé que estoy desenterrando una respuesta anterior aquí ... El problema aquí es que module.exports se define después de que necesita ClassB. (que muestra el enlace de JohnnyHK) Las dependencias circulares funcionan muy bien en Node, solo se definen sincrónicamente. Cuando se usan correctamente, en realidad resuelven muchos problemas de nodos comunes (como acceder a express.js appdesde otros archivos)

Solo asegúrese de que sus exportaciones necesarias estén definidas antes de requerir un archivo con una dependencia circular.

Esto se romperá:

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

module.exports = ClassA;

Esto funcionará:

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

Uso este patrón todo el tiempo para acceder a express.js appen otros archivos:

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app
Will Stern
fuente
2
gracias por compartir el patrón y luego compartir aún más cómo usas habitualmente este patrón cuando app = express()
exportas
34

A veces es realmente artificial introducir una tercera clase (como aconseja JohnnyHK), así que además de Ianzz: si desea reemplazar el module.exports, por ejemplo, si está creando una clase (como el archivo b.js en el ejemplo anterior), esto también es posible, solo asegúrese de que en el archivo que está comenzando la circular requiera, la declaración 'module.exports = ...' ocurre antes de la declaración require.

a.js (el archivo principal se ejecuta con el 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
Coen
fuente
gracias coen, nunca me había dado cuenta de que module.exports tenía un efecto en las dependencias circulares.
Laurent Perrin
Esto es especialmente útil con los modelos Mongoose (MongoDB); me ayuda a solucionar un problema cuando el modelo BlogPost tiene una matriz con referencias a comentarios, y cada modelo de Comentario tiene referencia al BlogPost.
Oleg Zarevennyi
14

La solución es 'declarar adelante' su objeto de exportación antes de requerir cualquier otro controlador. Entonces, si estructura todos sus módulos de esta manera y no se encontrará con ningún problema como ese:

// 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
fuente
3
En realidad, esto me llevó a usar simplemente en su exports.foo = function() {...}lugar. Definitivamente hizo el truco. ¡Gracias!
zanona
No estoy seguro de lo que estás proponiendo aquí. module.exportsya es un objeto simple por defecto, por lo que su línea de "declaración directa" es redundante.
ZachB
7

Se extiende una solución que requiere un cambio mínimo en module.exportslugar de anularla.

a.js: punto de entrada de la aplicación y módulo que usa el método do de 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: módulo que utiliza el método do 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 
    }
})

Funcionará y producirá:

doing b
doing a

Si bien este código no funcionará:

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

Salida:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function
setec
fuente
44
Si no tiene underscore, entonces los ES6 Object.assign()pueden hacer el mismo trabajo que _.extend()está haciendo en esta respuesta.
joeytwiddle
5

¿Qué pasa con la pereza que requiere solo cuando lo necesita? Entonces su b.js se ve de la siguiente manera

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

Por supuesto, es una buena práctica poner todas las declaraciones obligatorias en la parte superior del archivo. Pero no son las ocasiones, cuando me perdono para recoger algo fuera de un módulo de otro modo no relacionado. Llámalo hack, pero a veces es mejor que introducir una dependencia adicional, o agregar un módulo adicional o agregar nuevas estructuras (EventEmitter, etc.)

zevero
fuente
Y a veces es crítico cuando se trata de una estructura de datos de árbol con objetos secundarios que mantienen referencias a un padre. Gracias por el consejo.
Robert Oschler
5

Otro método que he visto hacer es exportar en la primera línea y guardarlo como una variable local como esta:

let self = module.exports = {};

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

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

Tiendo a usar este método, ¿conoces alguna desventaja?

Bence Gedai
fuente
puedes preferir hacer module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal
4

Puede resolver esto fácilmente: simplemente exporte sus datos antes de requerir cualquier otra cosa en los módulos donde usa 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
fuente
3

Similar a las respuestas de lanzz y setect, he estado usando el siguiente patrón:

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

Las Object.assign()copias de los miembros en el exportsobjeto que ya se ha dado a otros módulos.

La =asignación es lógicamente redundante, ya que solo se está configurando module.exportsa sí misma, pero la estoy usando porque ayuda a mi IDE (WebStorm) a reconocer que firstMemberes una propiedad de este módulo, por lo que "Ir a -> Declaración" (Cmd-B) y otras herramientas funcionarán desde otros archivos.

Este patrón no es muy bonito, por lo que solo lo uso cuando es necesario resolver un problema de dependencia cíclica.

joeytwiddle
fuente
2

Aquí hay una solución rápida que he encontrado uso completo.

En el archivo 'a.js'

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

En el archivo 'b.js' escriba lo siguiente

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

De esta manera, en la próxima iteración del evento, las clases de bucle se definirán correctamente y esas declaraciones obligatorias funcionarán como se esperaba.

Melik Karapetyan
fuente
1

En realidad terminé requiriendo mi dependencia con

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

No es bonita, pero funciona. Es más comprensible y honesto que cambiar b.js (por ejemplo, solo aumentar modules.export), que de lo contrario es perfecto como está.

zevero
fuente
De todas las soluciones en esta página, esta es la única que resolvió mi problema. Probé cada uno por turno.
Joe Lapp
0

Una forma de evitarlo es no requerir un archivo en otro, simplemente pasarlo como argumento a una función, lo que sea que necesite en otro archivo. De esta manera, nunca surgirá una dependencia circular.

sagar saini
fuente