Wie gehe ich mit zirkulären Abhängigkeiten mit RequireJS / AMD um?

79

In meinem System habe ich eine Reihe von "Klassen" in den Browser geladen, die während der Entwicklung jeweils separate Dateien enthalten und für die Produktion miteinander verkettet sind. Beim Laden initialisieren sie hier Gwie in diesem Beispiel eine Eigenschaft für ein globales Objekt :

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

Anstatt mein eigenes globales Objekt zu verwenden, denke ich darüber nach, jede Klasse zu einem eigenen AMD-Modul zu machen , basierend auf James Burkes Vorschlag :

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

Das Problem ist, dass es zuvor keine Abhängigkeit von der Deklarationszeit zwischen Mitarbeiter und Unternehmen gab: Sie konnten die Deklaration in die von Ihnen gewünschte Reihenfolge bringen, aber jetzt führt RequireJS eine Abhängigkeit ein, die hier (absichtlich) zirkulär ist Der obige Code schlägt fehl. Das addEmployee()Hinzufügen einer ersten Zeile var Employee = require("Employee");würde natürlich funktionieren , aber ich sehe diese Lösung als schlechter an, als RequireJS / AMD nicht zu verwenden, da ich als Entwickler diese neu erstellte zirkuläre Abhängigkeit kennen und etwas dagegen tun muss.

Gibt es eine bessere Möglichkeit, dieses Problem mit RequireJS / AMD zu lösen, oder verwende ich RequireJS / AMD für etwas, für das es nicht entwickelt wurde?

Avernet
quelle

Antworten:

59

Dies ist in der Tat eine Einschränkung im AMD-Format. Sie könnten Exporte verwenden, und dieses Problem verschwindet. Ich finde Exporte hässlich, aber so lösen reguläre CommonJS-Module das 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;
});

Andernfalls würde die Anforderung ("Mitarbeiter"), die Sie in Ihrer Nachricht erwähnen, ebenfalls funktionieren.

Im Allgemeinen müssen Sie bei Modulen die zirkulären Abhängigkeiten besser kennen, ob AMD oder nicht. Selbst in einfachem JavaScript müssen Sie sicherstellen, dass Sie ein Objekt wie das G-Objekt in Ihrem Beispiel verwenden.

jrburke
quelle
3
Ich dachte, Sie müssten Exporte in der Argumentliste beider Rückrufe deklarieren, wie function(exports, Company)und function(exports, Employee). Wie auch immer, danke für RequireJS, es ist großartig.
Sébastien RoccaSerra
@jrburke Ich denke, dies kann in eine Richtung richtig gemacht werden, für einen Mediator oder Kern oder eine andere Top-Down-Komponente? Ist das eine schreckliche Idee, es mit beiden Methoden zugänglich zu machen? stackoverflow.com/questions/11264827/…
SimplGy
1
Ich bin nicht sicher, ob ich verstehe, wie dies das Problem löst. Nach meinem Verständnis müssen alle Abhängigkeiten geladen werden, bevor die Definition ausgeführt wird. Ist das nicht der Fall, wenn "Exporte" als erste Abhängigkeit übergeben werden?
BT
1
Vermissen Sie nicht Exporte als Parameter in der Funktion?
Shabunc
1
Weitere Informationen zu @ shabuncs Punkt bezüglich des fehlenden Exportparameters finden Sie in dieser Frage: stackoverflow.com/questions/28193382/…
Michael.Lumley
15

Ich denke, dies ist ein ziemlicher Nachteil bei größeren Projekten, bei denen (mehrstufige) zirkuläre Abhängigkeiten unentdeckt bleiben. Mit madge können Sie jedoch eine Liste zirkulärer Abhängigkeiten drucken, um sich ihnen zu nähern.

madge --circular --format amd /path/src
Pascalius
quelle
CACSVML-13295: sc-admin-ui-express amills001c $ madge --circular --format amd ./ Keine zirkulären Abhängigkeiten gefunden!
Alexander Mills
8

Wenn Ihre Abhängigkeiten zu Beginn nicht geladen werden müssen (z. B. wenn Sie eine Klasse erweitern), können Sie Folgendes tun: (entnommen aus http://requirejs.org/docs/api.html# Rundschreiben )

In der Datei a.js:

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

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

    });

Und in der anderen Datei 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();
            }
        };

    });

Im Beispiel des OP würde sich dies folgendermaßen ändern:

    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");
    });
duftend
quelle
2
Wie Gili in seinem Kommentar sagte, ist diese Lösung falsch und wird nicht immer funktionieren. Es gibt eine Race-Bedingung, unter der der Codeblock zuerst ausgeführt wird.
Louis Ameline
6

Ich habe mir die Dokumente zu zirkulären Abhängigkeiten angesehen: http://requirejs.org/docs/api.html#circular

Wenn es eine zirkuläre Abhängigkeit mit a und b gibt, heißt es in Ihrem Modul, dass Sie als Abhängigkeit in Ihrem Modul Folgendes hinzufügen müssen:

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

dann, wenn Sie "a" brauchen, rufen Sie einfach "a" wie folgt:

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

Das hat bei mir funktioniert

yeahdixon
quelle
5

Ich würde nur die zirkuläre Abhängigkeit vermeiden. Vielleicht so etwas wie:

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

Ich denke nicht, dass es eine gute Idee ist, dieses Problem zu umgehen und zu versuchen, die zirkuläre Abhängigkeit beizubehalten. Fühlt sich einfach wie allgemeine schlechte Praxis an. In diesem Fall kann es funktionieren, da Sie diese Module wirklich benötigen, wenn die exportierte Funktion aufgerufen wird. Stellen Sie sich jedoch den Fall vor, in dem Module benötigt und in den eigentlichen Definitionsfunktionen selbst verwendet werden. Keine Problemumgehung wird diese Arbeit machen. Dies ist wahrscheinlich der Grund, warum require.js bei der Erkennung kreisförmiger Abhängigkeiten in den Abhängigkeiten der Definitionsfunktion schnell fehlschlägt.

Wenn Sie wirklich eine Problemumgehung hinzufügen müssen, muss die sauberere IMO eine Abhängigkeit gerade noch rechtzeitig erfordern (in diesem Fall in Ihren exportierten Funktionen), dann werden die Definitionsfunktionen einwandfrei ausgeführt. Aber noch sauberer IMO ist nur, um zirkuläre Abhängigkeiten insgesamt zu vermeiden, was sich in Ihrem Fall sehr einfach anfühlt.

Shu
quelle
2
Sie schlagen vor, ein Domänenmodell zu vereinfachen und weniger nutzbar zu machen, nur weil das Tool requirejs dies nicht unterstützt. Tools sollen Entwicklern das Leben erleichtern. Das Domain-Modell ist ziemlich einfach - Mitarbeiter und Unternehmen. Das Mitarbeiterobjekt sollte wissen, für welches Unternehmen er arbeitet, Unternehmen sollten eine Liste von Mitarbeitern haben. Das Domain-Modell ist richtig, es ist das Tool, das hier versagt
Dethariel
5

Alle veröffentlichten Antworten (außer https://stackoverflow.com/a/25170248/14731 ) sind falsch. Auch die offizielle Dokumentation (Stand November 2014) ist falsch.

Die einzige Lösung, die für mich funktioniert hat, besteht darin, eine "Gatekeeper" -Datei zu deklarieren und eine Methode zu definieren, die von den zirkulären Abhängigkeiten abhängt. Ein konkretes Beispiel finden Sie unter https://stackoverflow.com/a/26809254/14731 .


Hier ist, warum die oben genannten Lösungen nicht funktionieren.

  1. Du kannst nicht:
var a;
require(['A'], function( A ){
     a = new A();
});

und später verwenden a, da es keine Garantie gibt, dass dieser Codeblock vor dem verwendeten Codeblock ausgeführt wird a. (Diese Lösung ist irreführend, da sie in 90% der Fälle funktioniert.)

  1. Ich sehe keinen Grund zu der Annahme, dass dies exportsnicht für die gleiche Rennbedingung anfällig ist.

Die Lösung hierfür lautet:

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

    });

Jetzt können wir diese Module A und B in Modul C verwenden

//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
quelle
Übrigens, wenn Sie immer noch Probleme damit haben, sollte die Antwort von @ yeahdixon korrekt sein, und ich denke, die Dokumentation selbst ist korrekt.
Alexander Mills
Ich bin damit einverstanden, dass Ihre Methodik funktioniert, aber ich denke, die Dokumentation ist korrekt und könnte "synchron" einen Schritt näher kommen.
Alexander Mills
Sie können, weil alle Variablen beim Laden gesetzt sind. Es sei denn, Ihre Benutzer sind Zeitreisende und klicken auf die Schaltfläche, bevor sie existiert. Es wird die Kausalität brechen und dann ist eine Rassenbedingung möglich.
Eddie
0

In meinem Fall habe ich die zirkuläre Abhängigkeit gelöst, indem ich den Code des "einfacheren" Objekts in das komplexere verschoben habe. Für mich war das eine Sammlung und eine Modellklasse. Ich denke, in Ihrem Fall würde ich die mitarbeiterspezifischen Teile des Unternehmens zur Mitarbeiterklasse hinzufügen.

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

Ein bisschen hacky, aber es sollte für einfache Fälle funktionieren. Und wenn Sie addEmployeeeinen Mitarbeiter als Parameter umgestalten , sollte die Abhängigkeit für Außenstehende noch offensichtlicher sein.

Björn Tantau
quelle