Jak radzić sobie z zależnościami cyklicznymi z RequireJS / AMD?


80

W moim systemie mam kilka „klas” ładowanych do przeglądarki, z których każda jest oddzielnym plikiem podczas programowania i łączona razem w celu produkcji. Podczas ładowania inicjalizują one właściwość obiektu globalnego G, tak jak w tym przykładzie:

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

Zamiast używać mojego własnego globalnego obiektu, rozważam uczynienie każdej klasy własnym modułem AMD , zgodnie z sugestią Jamesa Burke'a :

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");
});

Problem polega na tym, że wcześniej nie było zależności między pracownikiem a firmą w czasie deklarowania: można było umieścić deklarację w dowolnej kolejności, ale teraz, używając RequireJS, wprowadza to zależność, która jest tutaj (celowo) okrężna, więc powyższy kod nie działa. Oczywiście w programie byłoby addEmployee()dodanie pierwszej liniivar Employee = require("Employee"); że to zadziałałoby , ale uważam to rozwiązanie za gorsze od nieużywania RequireJS / AMD, ponieważ wymaga ode mnie, programisty, bycia świadomym tej nowo utworzonej zależności cyklicznej i zrobienia czegoś z tym.

Czy istnieje lepszy sposób rozwiązania tego problemu w przypadku RequireJS / AMD, czy też używam RequireJS / AMD do czegoś, do czego nie został zaprojektowany?

Odpowiedzi:


59

To jest rzeczywiście ograniczenie w formacie AMD. Możesz użyć eksportu i ten problem zniknie. Uważam, że eksport jest brzydki, ale w ten sposób zwykłe moduły CommonJS rozwiązują problem:

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

W przeciwnym razie wymóg („Pracownik”), o którym wspominasz w wiadomości, również zadziała.

Ogólnie rzecz biorąc, w przypadku modułów musisz być bardziej świadomy zależności cyklicznych, AMD lub nie. Nawet w zwykłym JavaScript musisz upewnić się, że użyłeś obiektu takiego jak obiekt G. w twoim przykładzie.


3
Myślałem, że musisz zadeklarować eksport na liście argumentów obu wywołań zwrotnych, takich jak function(exports, Company)i function(exports, Employee). W każdym razie, dzięki za RequireJS, to wspaniale.
— Sébastien RoccaSerra

@jrburke Myślę, że można to zrobić jednokierunkowo, prawda, dla mediatora, rdzenia lub innego odgórnego komponentu? Czy to straszny pomysł, aby był dostępny za pomocą obu metod? stackoverflow.com/questions/11264827/…
— SimplGy,

1
Nie jestem pewien, czy rozumiem, jak to rozwiązuje problem. Rozumiem, że wszystkie zależności muszą zostać załadowane przed uruchomieniem definicji. Czy tak nie jest, jeśli „eksport” jest przekazywany jako pierwsza zależność?
— BT

1
nie tęsknisz za eksportem jako parametrem w funkcji?
— shabunc

1
Aby uzupełnić punkt @ shabunc dotyczący brakującego parametru eksportu, zobacz to pytanie: stackoverflow.com/questions/28193382/…
— Michael.Lumley

15

Myślę, że jest to dość wada w większych projektach, w których (wielopoziomowe) zależności cykliczne pozostają niewykryte. Jednak za pomocą madge możesz wydrukować listę zależności cyklicznych, aby się do nich zbliżyć.

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

CACSVML-13295: sc-admin-ui-express amills001c $ madge --circular --format amd ./ Nie znaleziono zależności cyklicznych!
— Alexander Mills

8

Jeśli nie potrzebujesz, aby Twoje zależności były ładowane na początku (np. Podczas rozszerzania klasy), możesz to zrobić: (pobrane z http://requirejs.org/docs/api.html# okrągły )

W pliku a.js:

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

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

    });

A w drugim pliku 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();
            }
        };

    });

W przykładzie PO wyglądałoby to następująco:

    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
Jak powiedział Gili w swoim komentarzu, to rozwiązanie jest złe i nie zawsze będzie działać. Istnieje warunek wyścigu, w którym blok kodu zostanie wykonany jako pierwszy.
— Louis Ameline

6

Przejrzałem dokumenty dotyczące zależności cyklicznych: http://requirejs.org/docs/api.html#circular

Jeśli istnieje zależność cykliczna z a i b, w module jest napisane, aby dodać wymaganie jako zależność w module:

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

wtedy gdy potrzebujesz „a” po prostu zadzwoń do „a” w ten sposób:

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

To zadziałało dla mnie


5

Po prostu uniknąłbym zależności cyklicznej. Może coś takiego:

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

Uważam, że obejście tego problemu i zachowanie zależności cyklicznej nie jest dobrym pomysłem. Po prostu wydaje się ogólną złą praktyką. W tym przypadku może to zadziałać, ponieważ naprawdę potrzebujesz tych modułów do wywołania eksportowanej funkcji. Ale wyobraź sobie przypadek, w którym moduły są wymagane i używane w samej definicji funkcji. Żadne obejście nie sprawi, że to zadziała. Prawdopodobnie dlatego require.js szybko kończy się niepowodzeniem po wykryciu cyklicznej zależności w zależnościach funkcji definicji.

Jeśli naprawdę musisz dodać obejście, prostszy IMO wymaga zależności w samą porę (w tym przypadku w twoich wyeksportowanych funkcjach), wtedy funkcje definicji będą działać poprawnie. Ale nawet czystsza IMO ma na celu całkowite uniknięcie zależności cyklicznych, co wydaje się naprawdę łatwe w twoim przypadku.


2
Sugerujesz uproszczenie modelu domeny i uczynienie go mniej użytecznym tylko dlatego, że narzędzie requirejs tego nie obsługuje. Narzędzia mają ułatwiać życie programistom. Model domeny jest dość prosty - pracownik i firma. Obiekt pracownika powinien wiedzieć, dla jakiej firmy (firm) pracuje, firmy powinny mieć listę pracowników. Model domeny ma rację, to narzędzie zawodzi tutaj
— Dethariel

5

Wszystkie opublikowane odpowiedzi (z wyjątkiem https://stackoverflow.com/a/25170248/14731 ) są błędne. Nawet oficjalna dokumentacja (stan na listopad 2014) jest błędna.

Jedynym rozwiązaniem, które zadziałało, jest zadeklarowanie pliku „strażnika” i zdefiniowanie w nim dowolnej metody, która zależy od zależności cyklicznych. Konkretny przykład można znaleźć pod adresem https://stackoverflow.com/a/26809254/14731 .


Oto dlaczego powyższe rozwiązania nie będą działać.

  1. Nie możesz:
var a;
require(['A'], function( A ){
     a = new A();
});

a następnie użyj go apóźniej, ponieważ nie ma gwarancji, że ten blok kodu zostanie wykonany przed blokiem kodu, który używa a. (To rozwiązanie jest mylące, ponieważ działa w 90% przypadków)

  1. Nie widzę powodu, by sądzić, że exportsnie jest narażony na te same warunki wyścigu.

rozwiązaniem tego jest:

//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);
        }

    });

teraz możemy użyć tych modułów A i B w module 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

    });

btw, jeśli nadal masz z tym problem, odpowiedź @ yeahdixon powinna być poprawna i myślę, że sama dokumentacja jest poprawna.
— Alexander Mills

Zgadzam się, że twoja metodologia działa, ale myślę, że dokumentacja jest poprawna i może być o krok bliżej do "synchronicznej".
— Alexander Mills

możesz, ponieważ wszystkie zmienne są ustawione podczas ładowania. Chyba że Twoi użytkownicy podróżują w czasie i kliknij przycisk, zanim on zaistnieje. To przełamie przyczynowość, a wtedy stan wyścigu będzie możliwy.
— Eddie

0

W moim przypadku rozwiązałem zależność cykliczną, przenosząc kod „prostszego” obiektu na bardziej złożony. Dla mnie to była kolekcja i klasa wzorcowa. Sądzę, że w twoim przypadku dodałbym do klasy Pracownik części firmy specyficzne dla pracownika.

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");
});

Trochę hacky, ale powinno działać w prostych przypadkach. A jeśli dokonasz refaktoryzacjiaddEmployee przyjmując pracownika jako parametr, zależność powinna być jeszcze bardziej oczywista dla osób z zewnątrz.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.