Как бороться с циклическими зависимостями в Node.js

162

В последнее время я работал с nodejs и до сих пор разбираюсь с системой модулей, поэтому извиняюсь, если это очевидный вопрос. Я хочу код примерно так, как показано ниже:

a.js (основной файл запускается с узлом)

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;

Кажется, моя проблема в том, что я не могу получить доступ к экземпляру ClassA из экземпляра ClassB.

Есть ли правильный / лучший способ структурировать модули для достижения того, что я хочу? Есть ли лучший способ поделиться переменными между модулями?

Runcible
источник
Я предлагаю вам взглянуть на разделение командных запросов, наблюдаемый шаблон и затем на то, что парни из CS называют менеджерами - что по сути является оболочкой для наблюдаемого шаблона.
dewwwald

Ответы:

86

Хотя node.js допускает циклические requireзависимости, как вы выяснили, это может быть довольно грязно, и вам, вероятно, лучше реструктурировать свой код, чтобы он не требовался. Может быть, создать третий класс, который использует два других, чтобы выполнить то, что вам нужно.

JohnnyHK
источник
6
+1 Это правильный ответ. Круговые зависимости - это запах кода. Если A и B всегда используются вместе, они фактически являются единым модулем, поэтому объедините их. Или найти способ разорвать зависимость; может быть, это сложный шаблон.
Джеймс
94
Не всегда. например, в моделях базы данных, если у меня есть модели A и B, в модели AI может потребоваться ссылка на модель B (например, для объединения операций), и наоборот. Поэтому лучше использовать несколько параметров A и B (те, которые не зависят от других модулей) перед использованием функции require.
Жоау Бруно Абу Хатем де Лиз
11
Я также не вижу циклические зависимости как запах кода. Я разрабатываю систему, в которой есть несколько случаев, когда это необходимо. Например, команды моделирования и пользователи, где пользователи могут принадлежать многим командам. Так что не то, чтобы что-то не так с моим моделированием. Очевидно, я мог бы реорганизовать свой код, чтобы избежать циклической зависимости между двумя сущностями, но это не будет самой чистой формой модели предметной области, поэтому я не буду этого делать.
Александр Мартини
1
Тогда я должен ввести зависимость при необходимости, это то, что вы имеете в виду? Использование третьего для управления взаимодействием двух зависимостей с циклической проблемой?
giovannipds
2
Это не грязно ... кто-то может захотеть затормозить файл, чтобы избежать книги кодов в одном файле. Как предполагает узел, вы должны добавить exports = {}вверху кода, а затем exports = yourDataв конце кода. С этой практикой вы избежите почти всех ошибок от циклических зависимостей.
Пристон
178

Попробуйте установить свойства module.exportsвместо полной замены. Например, module.exports.instance = new ClassA()в a.js, module.exports.ClassB = ClassBв b.js. Когда вы создаете циклические зависимости модуля, запрашиваемый модуль получает ссылку на неполное module.exportsиз требуемого модуля, к которому вы можете добавить другие свойства, но при установке целого module.exportsвы фактически создаете новый объект, которого у запрашивающего модуля нет способ доступа.

lanzz
источник
6
Это может быть все верно, но я бы сказал, все же избегайте циклических зависимостей. Создание специальных мер для работы с модулями, которые имеют не полностью загруженные звуки, как будто это создаст будущую проблему, которую вы не хотите иметь. Этот ответ предписывает решение, как обращаться с не полностью загруженными модулями ... Я не думаю, что это хорошая идея.
Александр Миллс
1
Как бы вы поместили конструктор класса module.exportsбез его полной замены, чтобы позволить другим классам «конструировать» экземпляр класса?
Тим Визе
1
Я не думаю, что ты можешь. Модули, которые уже импортировали ваш модуль, не смогут увидеть это изменение
lanzz
52

[РЕДАКТИРОВАТЬ] это не 2015, и большинство библиотек (т.е. экспресс) сделали обновления с лучшими шаблонами, поэтому циклические зависимости больше не нужны. Я рекомендую просто не использовать их .


Я знаю, что выкопал старый ответ здесь ... Проблема здесь в том, что module.exports определяется после того, как вам требуется ClassB. (что показывает ссылка JohnnyHK) Круговые зависимости прекрасно работают в Node, они просто определяются синхронно. При правильном использовании они фактически решают много общих проблем с узлами (например, доступ к express.jsapp из других файлов)

Просто убедитесь, что ваш необходимый экспорт определен до вам потребуется файл с круговой зависимостью.

Это сломается:

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

module.exports = ClassA;

Это будет работать:

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

Я постоянно использую этот шаблон для доступа к express.js appв других файлах:

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app
Уилл Стерн
источник
2
спасибо за то, что поделились шаблоном, а затем рассказали, как вы обычно используете этот шаблон при экспортеapp = express()
user566245
34

Иногда очень сложно ввести третий класс (как советует JohnnyHK), поэтому в дополнение к Ianzz: если вы хотите заменить module.exports, например, если вы создаете класс (например, файл b.js в В приведенном выше примере) это также возможно, просто убедитесь, что в файле, который запускает циклическое требование, оператор 'module.exports = ...' происходит перед оператором требования.

a.js (основной файл запускается с узлом)

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
источник
спасибо, коэн, я никогда не понимал, что module.exports влияет на циклические зависимости.
Лоран Перрен
это особенно полезно с моделями Mongoose (MongoDB); помогает мне решить проблему, когда модель BlogPost имеет массив со ссылками на комментарии, а каждая модель Comment имеет ссылку на BlogPost.
Олег Заревенный
14

Решение состоит в том, чтобы «объявить вперед» ваш объект экспорта, прежде чем требовать какого-либо другого контроллера. Поэтому, если вы структурируете все свои модули, как это, и у вас не возникнет таких проблем:

// 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
На самом деле, это заставило меня просто использовать exports.foo = function() {...}вместо этого. Определенно сделал свое дело. Спасибо!
Занона
Я не уверен, что вы предлагаете здесь. module.exportsпо умолчанию уже является обычным объектом, поэтому ваша строка "предварительного объявления" является избыточной.
ЗакБ
7

Решение, которое требует минимальных изменений, расширяется module.exports а не переопределяет его.

a.js - точка входа и модуль приложения, которые используют метод 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 - модуль который использует метод 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 
    }
})

Будет работать и производить:

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

Вывод:

node a.js
b.js:7
a.do();
    ^    
TypeError: a.do is not a function
Setec
источник
4
Если у вас нет underscore, то ES6 Object.assign()может сделать ту же работу, что_.extend() и в этом ответе.
Joeytwiddle
5

Как насчет ленивых, требующих только тогда, когда это необходимо? Итак, ваш b.js выглядит следующим образом

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

Конечно, хорошей практикой является размещение всех требуемых операторов в верхней части файла. Но бывают случаи, когда я прощаю себя за то, что выбрал что-то из другого, не связанного с этим модуля. Называйте это хаком, но иногда это лучше, чем вводить дополнительную зависимость или добавлять дополнительный модуль или добавлять новые структуры (EventEmitter и т. Д.)

ЗЕВЕРО
источник
И иногда это важно при работе с древовидной структурой данных с дочерними объектами, поддерживающими ссылки на родительский объект. Спасибо за чаевые.
Роберт Ошлер
5

Другой метод, который, как я видел, делают люди, - это экспорт в первой строке и сохранение его как локальной переменной, например:

let self = module.exports = {};

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

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

Я склонен использовать этот метод, знаете ли вы о его недостатках?

Бенс Гедай
источник
Вы можете скорее сделать module.exports.func1 = ,module.exports.func2 =
Ашвани Агарвал
4

Вы можете легко решить эту проблему: просто экспортируйте свои данные, прежде чем требовать что-либо еще в модулях, где вы используете 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

Как и в ответах Lanzz и Setect, я использовал следующую схему:

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

В Object.assign()копирует член в exportsобъект , который уже был дан к другим модулям.

=Назначение логически излишним, так как он просто установив module.exportsдля себя, но я использую его , потому что это помогает мой IDE (WebStorm) признать , что firstMemberэто свойство этого модуля, так что «Go To -> Декларация» (Cmd-B) и другие инструменты будут работать из других файлов.

Этот шаблон не очень хорош, поэтому я использую его только тогда, когда нужно решить проблему циклической зависимости.

joeytwiddle
источник
2

Вот быстрый обходной путь, который я нашел полным.

В файле «a.js»

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

В файле «b.js» напишите следующее

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

Таким образом, на следующей итерации классы цикла событий будут определены правильно, а операторы require будут работать как положено.

Мелик Карапетян
источник
1

На самом деле я в конечном итоге требует моей зависимости с

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

не красиво, но это работает. Это более понятно и честно, чем изменение b.js (например, только расширение модуля. Экспорт), который в остальном идеален как есть.

ЗЕВЕРО
источник
Из всех решений на этой странице это единственное, что решило мою проблему. Я попробовал каждый по очереди.
Джо Лапп
0

Один из способов избежать этого - не требовать, чтобы один файл находился в другом, просто передайте его в качестве аргумента функции, которая вам когда-либо понадобится в другом файле. Таким образом, круговая зависимость никогда не возникнет.

Sagar Saini
источник