Como lidar com dependências cíclicas no Node.js

162

Eu tenho trabalhado com o nodejs ultimamente e ainda estou familiarizado com o sistema de módulos, então peço desculpas se esta for uma pergunta óbvia. Quero código aproximadamente como o seguinte abaixo:

a.js (o arquivo principal executado com o nó)

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;

Meu problema parece ser que não consigo acessar a instância da ClassA de dentro de uma instância da ClassB.

Existe uma maneira correta / melhor de estruturar os módulos para alcançar o que eu quero? Existe uma maneira melhor de compartilhar variáveis ​​entre módulos?

Runcible
fonte
Sugiro que você procure comandos de separação de consulta, padrão observável e, em seguida, o que os caras do CS chamam de gerentes - que é basicamente um invólucro para o padrão observável.
dewwwald

Respostas:

86

Embora o node.js permita requiredependências circulares , como você descobriu, pode ser bastante complicado e provavelmente é melhor reestruturar seu código para não precisar dele. Talvez crie uma terceira classe que use as outras duas para realizar o que você precisa.

JohnnyHK
fonte
6
+1 Esta é a resposta certa. Dependências circulares têm cheiro de código. Se A e B são sempre usados ​​juntos, eles são efetivamente um único módulo, portanto, mescle-os. Ou encontre uma maneira de quebrar a dependência; talvez seja um padrão composto.
James
94
Nem sempre. nos modelos de banco de dados, por exemplo, se eu tenho os modelos A e B, no modelo AI pode fazer referência ao modelo B (por exemplo, para unir operações) e vice-versa. Portanto, exporte várias propriedades A e B (aquelas que não dependem de outros módulos) antes de usar a função "requereu" pode ser uma resposta melhor.
João Bruno Abou Hatem de Liz
11
Também não vejo dependências circulares como cheiro de código. Estou desenvolvendo um sistema em que há alguns casos em que é necessário. Por exemplo, modelando equipes e usuários, onde os usuários podem pertencer a muitas equipes. Portanto, não é que algo esteja errado com a minha modelagem. Obviamente, eu poderia refatorar meu código para evitar a dependência circular entre as duas entidades, mas essa não seria a forma mais pura do modelo de domínio, portanto não farei isso.
Alexandre Martini
1
Então devo injetar a dependência quando necessário, é isso que você quer dizer? Usando um terceiro para controlar a interação entre as duas dependências com o problema cíclico?
Giovannipds
2
Isso não é confuso. Alguém pode querer travar um arquivo para evitar que um livro de códigos seja um arquivo único. Como o nó sugere, você deve adicionar um exports = {}na parte superior do seu código e depois exports = yourDatano final do seu código. Com esta prática, você evitará quase todos os erros das dependências circulares.
prieston 29/01
178

Tente ativar as propriedades module.exports, em vez de substituí-las completamente. Por exemplo, module.exports.instance = new ClassA()dentro a.js, module.exports.ClassB = ClassBdentro b.js. Quando você cria dependências de módulos circulares, o módulo exigente obtém uma referência incompleta module.exportsdo módulo necessário, no qual você pode adicionar outras propriedades posteriormente, mas quando define o conjunto module.exports, na verdade, cria um novo objeto que o módulo exigente não possui. maneira de acessar.

lanzz
fonte
6
Isso pode ser verdade, mas eu diria que ainda evita dependências circulares. Fazer arranjos especiais para lidar com módulos que tenham sons carregados incompletamente, criará um problema futuro que você não deseja ter. Esta resposta prescreve uma solução para como lidar com módulos incompletamente carregados ... Não acho que seja uma boa ideia.
Alexander Mills
1
Como você colocaria um construtor de classe module.exportssem substituí-lo completamente, para permitir que outras classes 'construíssem' uma instância da classe?
Tim Visée 7/08/16
1
Eu acho que você não pode. Os módulos que importou o seu módulo já não será capaz de ver que a mudança
lanzz
52

[EDIT] não é 2015 e a maioria das bibliotecas (ou seja, express) fez atualizações com melhores padrões, de modo que as dependências circulares não são mais necessárias. Eu recomendo simplesmente não usá-los .


Eu sei que estou desenterrando uma resposta antiga aqui ... O problema aqui é que module.exports é definido depois que você precisa da ClassB. (que o link de JohnnyHK mostra) As dependências circulares funcionam muito bem no Node, apenas são definidas de forma síncrona. Quando usados ​​corretamente, eles realmente resolvem muitos problemas comuns de nó (como acessar o express.js appde outros arquivos)

Apenas verifique se as exportações necessárias estão definidas antes de você precisar de um arquivo com uma dependência circular.

Isso vai quebrar:

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

module.exports = ClassA;

Isso funcionará:

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

Eu uso esse padrão o tempo todo para acessar o express.js appem outros arquivos:

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app
Will Stern
fonte
2
obrigado por compartilhar o padrão e depois compartilhar como você costuma usar esse padrão ao exportarapp = express()
user566245
34

Às vezes, é realmente artificial introduzir uma terceira classe (como JohnnyHK aconselha), portanto, além de Ianzz: Se você deseja substituir o module.exports, por exemplo, se você estiver criando uma classe (como o arquivo b.js em no exemplo acima), isso também é possível, apenas certifique-se de que, no arquivo que está iniciando a requisição circular, a instrução 'module.exports = ...' ocorra antes da declaração de exigência.

a.js (o arquivo principal executado com o nó)

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
fonte
obrigado coen, eu nunca tinha percebido que module.exports tinha um efeito sobre dependências circulares.
23713 Laurent Perrin
isso é especialmente útil nos modelos Mongoose (MongoDB); ajuda-me a corrigir um problema quando o modelo do BlogPost tem uma matriz com referências a comentários e cada modelo de comentário tem referência ao BlogPost.
Oleg Zarevennyi 26/03/19
14

A solução é 'declarar adiante' seu objeto de exportação antes de exigir qualquer outro controlador. Portanto, se você estruturar todos os seus módulos como este e não terá problemas assim:

// 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
fonte
3
Na verdade, isso me levou a simplesmente usar exports.foo = function() {...}. Definitivamente fez o truque. Obrigado!
Zanona
Não tenho certeza do que você está propondo aqui. module.exportsjá é um objeto simples, por padrão, portanto, sua linha de "declaração direta" é redundante.
ZachB 21/04
7

Uma solução que requer alterações mínimas está se estendendo em module.exportsvez de substituí-la.

a.js - ponto de entrada do aplicativo e módulo que usam o método do 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 usa o método do 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á e produzirá:

doing b
doing a

Embora este código não funcione:

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

Resultado:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function
setec
fonte
4
Se você não possui underscore, os ES6 Object.assign()podem fazer o mesmo trabalho que _.extend()está fazendo nesta resposta.
Joeytwiddle #
5

E quanto à preguiça de exigir apenas quando você precisar? Portanto, seu b.js tem a seguinte aparência

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

Obviamente, é uma boa prática colocar todas as instruções de exigência no topo do arquivo. Mas não são ocasiões, onde eu me perdoar por escolher algo fora de um módulo de outro modo não relacionado. Chame isso de hack, mas às vezes isso é melhor do que introduzir uma dependência adicional ou adicionar um módulo extra ou adicionar novas estruturas (EventEmitter, etc)

zevero
fonte
E, às vezes, é crítico ao lidar com uma estrutura de dados em árvore com objetos filhos, mantendo referências a um pai. Obrigado pela dica.
Robert Oschler 10/02/19
5

Um outro método que eu vi as pessoas fazer é exportar na primeira linha e salvá-lo como uma variável local como esta:

let self = module.exports = {};

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

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

Eu costumo usar esse método, você conhece alguma desvantagem dele?

Bence Gedai
fonte
você pode preferir fazer module.exports.func1 = ,module.exports.func2 =
Ashwani Agarwal
4

Você pode resolver isso facilmente: basta exportar seus dados antes de precisar de mais alguma coisa nos módulos em que você 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
fonte
3

Semelhante às respostas de lanzz e setect, tenho usado o seguinte padrão:

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

As Object.assign()cópias são copiadas para o exportsobjeto que já foi fornecido para outros módulos.

A =atribuição é logicamente redundante, pois está apenas se configurando module.exports, mas eu a estou usando porque ajuda meu IDE (WebStorm) a reconhecer que firstMemberé uma propriedade deste módulo, então "Vá para -> Declaração" (Cmd-B) e outras ferramentas funcionarão com outros arquivos.

Esse padrão não é muito bonito; portanto, eu o uso apenas quando um problema de dependência cíclica precisa ser resolvido.

joeytwiddle
fonte
2

Aqui está uma solução rápida que eu achei totalmente útil.

No arquivo 'a.js'

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

No arquivo 'b.js', escreva o seguinte

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

Dessa maneira, na próxima iteração das classes do loop de eventos, será definido corretamente e as instruções de solicitação funcionarão conforme o esperado.

Melik Karapetyan
fonte
1

Na verdade, acabei exigindo minha dependência de

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

Não é bonito, mas funciona. É mais compreensível e honesto do que alterar o b.js (por exemplo, apenas aumentando o modules.export), que de outra forma é perfeito como está.

zevero
fonte
De todas as soluções nesta página, esta é a única que resolveu meu problema. Eu tentei cada um por sua vez.
quer
0

Uma maneira de evitá-lo é não exigir um arquivo em outro, apenas passá-lo como argumento para uma função do que você precisar em outro arquivo. Dessa maneira, a dependência circular nunca surgirá.

sagar saini
fonte