Як обробляти кругові залежності за допомогою RequireJS / AMD?


80

У моїй системі я маю декілька "класів", завантажених у браузер окремими файлами під час розробки та об'єднаних разом для виробництва. Під час завантаження вони ініціалізують властивість для глобального об’єкта тут G, як у цьому прикладі:

var G = {};

G.Employee = function(name) {
    this.name = name;
    this.company = new G.Company(name + "'s own company");
};

G.Company = function(name) {
    this.name = name;
    this.employees = [];
};
G.Company.prototype.addEmployee = function(name) {
    var employee = new G.Employee(name);
    this.employees.push(employee);
    employee.company = this;
};

var john = new G.Employee("John");
var bigCorp = new G.Company("Big Corp");
bigCorp.addEmployee("Mary");

Замість того, щоб використовувати свій власний глобальний об'єкт, я розглядаю можливість створити для кожного класу власний модуль AMD на основі пропозиції Джеймса Берка :

define("Employee", ["Company"], function(Company) {
    return function (name) {
        this.name = name;
        this.company = new Company(name + "'s own company");
    };
});
define("Company", ["Employee"], function(Employee) {
    function Company(name) {
        this.name = name;
        this.employees = [];
    };
    Company.prototype.addEmployee = function(name) {
        var employee = new Employee(name);
        this.employees.push(employee);
        employee.company = this;
    };
    return Company;
});
define("main", ["Employee", "Company"], function (Employee, Company) {
    var john = new Employee("John");
    var bigCorp = new Company("Big Corp");
    bigCorp.addEmployee("Mary");
});

Проблема в тому, що раніше між співробітником і компанією не було залежності від часу оголошення: ви могли розміщувати декларацію в будь-якому порядку, який хочете, але тепер, використовуючи RequireJS, це вводить залежність, яка тут (навмисно) кругова, тому вище коду не вдається. Звичайно addEmployee(), якщо додати перший рядок var Employee = require("Employee");, це спрацює , але я бачу це рішення гіршим за невикористання RequireJS / AMD, оскільки мені, розробнику, потрібно знати про цю нещодавно створену циркулярну залежність і щось робити з цим.

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

Відповіді:


59

Це справді обмеження у форматі AMD. Ви можете використовувати експорт, і ця проблема зникає. Я вважаю експорт потворним, але це те, як звичайні модулі CommonJS вирішують проблему:

define("Employee", ["exports", "Company"], function(exports, Company) {
    function Employee(name) {
        this.name = name;
        this.company = new Company.Company(name + "'s own company");
    };
    exports.Employee = Employee;
});
define("Company", ["exports", "Employee"], function(exports, Employee) {
    function Company(name) {
        this.name = name;
        this.employees = [];
    };
    Company.prototype.addEmployee = function(name) {
        var employee = new Employee.Employee(name);
        this.employees.push(employee);
        employee.company = this;
    };
    exports.Company = Company;
});

В іншому випадку вимога ("Працівник"), яку ви згадаєте у своєму повідомленні, також спрацює.

Взагалі з модулями вам слід більше знати про кругові залежності, AMD чи ні. Навіть у звичайному JavaScript ви повинні обов’язково використовувати такий об’єкт, як об’єкт G у вашому прикладі.


3
Я думав, вам потрібно оголосити експорт у списку аргументів обох зворотних викликів, як function(exports, Company)і function(exports, Employee). У будь-якому випадку, дякую за RequireJS, це неймовірно.
Sébastien RoccaSerra

@jrburke Я думаю, що це можна зробити в одному напрямку, правильно, для посередника, ядра чи іншого компонента зверху вниз? Це страшна ідея, зробити її доступною за допомогою обох методів? stackoverflow.com/questions/11264827 / ...
SimplGy

1
Я не впевнений, що розумію, як це вирішує проблему. Я розумію, що всі залежності повинні бути завантажені до запуску визначення. Хіба це не так, якщо "експорт" передається як перша залежність?
BT

1
ви не пропускаєте експорт як параметр у функції?
shabunc

1
Щоб продовжити питання @ shabunc щодо відсутнього параметра експорту, див. Це запитання: stackoverflow.com/questions/28193382/…
Michael.Lumley

15

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

madge --circular --format amd /path/src

CACSVML-13295: sc-admin-ui-express amills001c $ madge --circular --format amd. / Кругових залежностей не знайдено!
Олександр Міллс

8

Якщо вам не потрібно завантажувати свої залежності на початку (наприклад, коли ви розширюєте клас), то ось що ви можете зробити: (взято з http://requirejs.org/docs/api.html# круговий )

У файлі a.js:

    define( [ 'B' ], function( B ){

        // Just an example
        return B.extend({
            // ...
        })

    });

А в іншому файлі b.js:

    define( [ ], function( ){ // Note that A is not listed

        var a;
        require(['A'], function( A ){
            a = new A();
        });

        return function(){
            functionThatDependsOnA: function(){
                // Note that 'a' is not used until here
                a.doStuff();
            }
        };

    });

У прикладі OP це змінилося б так:

    define("Employee", [], function() {

        var Company;
        require(["Company"], function( C ){
            // Delayed loading
            Company = C;
        });

        return function (name) {
            this.name = name;
            this.company = new Company(name + "'s own company");
        };
    });

    define("Company", ["Employee"], function(Employee) {
        function Company(name) {
            this.name = name;
            this.employees = [];
        };
        Company.prototype.addEmployee = function(name) {
            var employee = new Employee(name);
            this.employees.push(employee);
            employee.company = this;
        };
        return Company;
    });

    define("main", ["Employee", "Company"], function (Employee, Company) {
        var john = new Employee("John");
        var bigCorp = new Company("Big Corp");
        bigCorp.addEmployee("Mary");
    });

2
Як сказав Гілі у своєму коментарі, це рішення є неправильним і не завжди буде працювати. Існує умова перегонів, при якому блок коду буде виконаний першим.
Louis Ameline

6

Я переглянув документи про кругові залежності: http://requirejs.org/docs/api.html#circular

Якщо існує кругова залежність з a та b, у вашому модулі написано, що потрібно додати require як залежність у вашому модулі, ось так:

define(["require", "a"],function(require, a) { ....

тоді, коли вам потрібно "a", просто зателефонуйте "a" приблизно так:

return function(title) {
        return require("a").doSomething();
    }

Це спрацювало для мене


5

Я просто уникну кругової залежності. Може щось на зразок:

G.Company.prototype.addEmployee = function(employee) {
    this.employees.push(employee);
    employee.company = this;
};

var mary = new G.Employee("Mary");
var bigCorp = new G.Company("Big Corp");
bigCorp.addEmployee(mary);

Я не думаю, що це хороша ідея обійти це питання і спробувати зберегти циркулярну залежність. Просто це схоже на загальну погану практику. У цьому випадку це може спрацювати, оскільки вам дійсно потрібні ці модулі для того, коли викликається експортована функція. Але уявіть випадок, коли модулі потрібні і використовуються у власне самих функціях визначення. Жодне обхідне рішення не змусить цього працювати. Ось, мабуть, тому require.js швидко виходить з ладу при круговому виявленні залежностей у залежностях функції визначення.

Якщо вам дійсно доведеться попрацювати, чистіший IMO вимагатиме залежності вчасно (у цьому випадку у ваших експортованих функціях), тоді функції визначення будуть працювати нормально. Але навіть чистіший IMO полягає лише у тому, щоб взагалі уникнути кругових залежностей, що у вашому випадку дійсно легко зробити.


2
Ви пропонуєте спростити модель домену та зробити її менш корисною лише тому, що інструмент requirejs цього не підтримує. Інструменти повинні полегшити життя розробника. Модель домену досить проста - співробітник та компанія. Об'єкт працівника повинен знати, в якій компанії (компаніях) він працює, компанії повинні мати список працівників. Модель домену правильна, тут виходить з ладу інструмент
Dethariel

5

Усі розміщені відповіді (крім https://stackoverflow.com/a/25170248/14731 ) є помилковими. Навіть офіційна документація (станом на листопад 2014 року) помилкова.

Єдине рішення, яке працювало для мене, - це оголосити файл "воротаря" і запропонувати йому визначити будь-який метод, який залежить від кругових залежностей. Дивіться https://stackoverflow.com/a/26809254/14731 для конкретного прикладу.


Ось чому вищевказані рішення не будуть працювати.

  1. Ти не можеш:
var a;
require(['A'], function( A ){
     a = new A();
});

а потім використовувати aпізніше, тому що немає гарантії, що цей код коду буде виконаний перед блоком коду, який використовує a. (Це рішення вводить в оману, оскільки воно працює у 90% випадків)

  1. Я не бачу підстав вважати, що exportsне є вразливим до того самого стану раси.

рішення цього:

//module A

    define(['B'], function(b){

       function A(b){ console.log(b)}

       return new A(b); //OK as is

    });


//module B

    define(['A'], function(a){

         function B(a){}

         return new B(a);  //wait...we can't do this! RequireJS will throw an error if we do this.

    });


//module B, new and improved
    define(function(){

         function B(a){}

       return function(a){   //return a function which won't immediately execute
              return new B(a);
        }

    });

тепер ми можемо використовувати ці модулі A і B в модулі C

//module C
    define(['A','B'], function(a,b){

        var c = b(a);  //executes synchronously (no race conditions) in other words, a is definitely defined before being passed to b

    });

До речі, якщо у вас все ще виникають проблеми з цим, відповідь @ yeahdixon повинна бути правильною, і я думаю, що сама документація є правильною.
Олександр Міллс

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

Ви можете, оскільки всі змінні встановлюються під навантаженням. Якщо ваші користувачі не подорожують у часі і не натиснуть кнопку, перш ніж вона існує. Це порушить причинність і тоді можливі умови перегонів.
Едді

0

У моєму випадку я вирішив циклічну залежність, перемістивши код "простішого" об'єкта в більш складний. Для мене це була колекція та модельний клас. Я гадаю, у вашому випадку я додав би спеціальні частини компанії до класу Employee.

define("Employee", ["Company"], function(Company) {
    function Employee (name) {
        this.name = name;
        this.company = new Company(name + "'s own company");
    };
    Company.prototype.addEmployee = function(name) {
        var employee = new Employee(name);
        this.employees.push(employee);
        employee.company = this;
    };

    return Employee;
});
define("Company", [], function() {
    function Company(name) {
        this.name = name;
        this.employees = [];
    };
    return Company;
});
define("main", ["Employee", "Company"], function (Employee, Company) {
    var john = new Employee("John");
    var bigCorp = new Company("Big Corp");
    bigCorp.addEmployee("Mary");
});

Трохи хакі, але це має працювати для простих випадків. І якщо ви рефакторируете addEmployeeвзяти працівника як параметр, залежність повинна бути ще більш очевидною для сторонніх.

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