Як боротися з циклічними залежностями в 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.

Чи є правильний / кращий спосіб структурувати модулі для досягнення того, що я хочу? Чи є кращий спосіб поділити змінні між модулями?


Я пропоную вам заглянути для розділення запитів, шаблону, що спостерігається, а потім того, що хлопці CS називають менеджерами - що в основному є обгорткою для шаблону, що спостерігається.
dewwwald

Відповіді:


86

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


6
+1 Це правильна відповідь. Кругові залежності - кодовий запах. Якщо A і B завжди використовуються разом, вони фактично є єдиним модулем, тому об'єднайте їх. Або знайти спосіб подолання залежності; можливо, це складова картина.
Джеймс

94
Не завжди. наприклад, у моделях баз даних, якщо у мене є моделі A і B, в моделі AI, можливо, потрібно посилатися на модель B (наприклад, для об'єднання операцій), і навпаки. Тому експорт декількох властивостей A і B (тих, які не залежать від інших модулів) перед використанням функції "вимагати" може бути кращою відповіддю.
Жоао Бруно Абу Хатем де Ліз

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, ви фактично створюєте новий об'єкт, якого у необхідного модуля немає спосіб доступу.


6
Це може бути правдою, але я б сказав, що все-таки уникати кругових залежностей. Створення спеціальних домовленостей для роботи з модулями, які мають неповно завантажені звуки, як це створить майбутню проблему, якої ви не хочете мати. Ця відповідь прописує рішення, як поводитися з неповно завантаженими модулями ... Я не думаю, що це гарна ідея.
Олександр Міллс

1
Як би ви поставили конструктор класів, module.exportsне повністю заміняючи його, щоб інші класи могли «сконструювати» екземпляр класу?
Tim Visée

1
Я не думаю, що ти можеш. Модулі, які вже імпортували ваш модуль, не зможуть побачити цю зміну
lanzz

52

[EDIT] це не 2015 рік, і більшість бібліотек (тобто експрес) оновлювали кращі шаблони, тому кругові залежності більше не потрібні. Я рекомендую їх просто не використовувати .


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

Просто переконайтеся, що необхідний експорт визначено перед тим, як вимагати файл із круговою залежністю.

Це порушить:

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()
поділитесь

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

дякую коен, я ніколи не розумів, що module.exports впливає на кругові залежності.
Лоран Перрін

це особливо корисно для моделей Mongoose (MongoDB); допомагає мені вирішити проблему, коли модель BlogPost має масив із посиланнями на коментарі, і кожна модель коментаря має посилання на 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() {...}. Виразно зробив трюк. Дякую!
zanona

Я не впевнений, що ти тут пропонуєш. module.exportsє звичайним Об'єктом за замовчуванням, тож ваш рядок "вперед оголошення" є зайвим.
ZachB

7

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

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

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) та інші інструменти працюватимуть з інших файлів.

Цей шаблон не дуже гарний, тому я використовую його лише тоді, коли проблему циклічної залежності потрібно вирішити.


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

Таким чином на наступній ітерації класів циклу подій буде визначено правильно, і ті, хто вимагає операцій, працюватимуть, як очікувалося.


1

Насправді я зрештою вимагав своєї залежності

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

не дуже, але це працює. Це зрозуміліше і чесніше, ніж зміна b.js (наприклад, лише розширення модулів.експорт), що в іншому випадку ідеально, як є.


З усіх рішень на цій сторінці це єдине, що вирішило мою проблему. Я намагався кожен по черзі.
Джо Лапп

0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.