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?

avernet
źródło

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.

jrburke
źródło
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
Pascalius
źródło
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");
    });
wonny
źródło
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

yeahdixon
źródło
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.

Shu
źródło
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

    });
Gili
źródło
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.

Björn Tantau
źródło