web-dev-qa-db-fra.com

Comment gérer les dépendances circulaires avec RequireJS/AMD?

Dans mon système, un certain nombre de "classes" sont chargées dans le navigateur, chacune d’elles étant des fichiers distincts au cours du développement, puis concaténée pour la production. Au fur et à mesure qu'ils sont chargés, ils initialisent une propriété sur un objet global, ici G, comme dans cet exemple:

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

Au lieu d’utiliser mon propre objet global, j’envisage de faire de chaque classe son propre module AMD , basé sur la suggestion de James Burke :

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

Le problème est qu’avant, il n’existait pas de dépendance déclarée dans le temps entre Employé et Société: vous pouviez placer la déclaration dans l’ordre que vous souhaitiez, mais maintenant, avec RequireJS, cela introduit une dépendance, qui est ici (intentionnellement) circulaire. le code ci-dessus échoue. Bien sûr, dans addEmployee(), ajouter une première ligne var Employee = require("Employee"); ferait le ferait fonctionner , mais je considère que cette solution est inférieure au fait de ne pas utiliser RequireJS/AMD, car cela nécessite que le développeur prenne conscience de cette dépendance circulaire nouvellement créée et faire quelque chose à ce sujet.

Existe-t-il un meilleur moyen de résoudre ce problème avec RequireJS/AMD, ou est-ce que j'utilise RequireJS/AMD pour quelque chose pour lequel il n'a pas été conçu?

76
avernet

Ceci est en effet une restriction dans le format AMD. Vous pouvez utiliser les exportations et ce problème disparaît. Je trouve que les exportations sont laides, mais voici comment les modules CommonJS standard résolvent le problème:

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

Sinon, l'exigence («employé») que vous mentionnez dans votre message fonctionnerait également.

En général, avec les modules, vous devez être plus conscient des dépendances circulaires, qu’elles soient ou non sous AMD. Même en JavaScript simple, vous devez être sûr d'utiliser un objet tel que l'objet G dans votre exemple.

59
jrburke

Je pense que cela constitue un inconvénient pour les projets plus importants dans lesquels les dépendances circulaires (à plusieurs niveaux) demeurent non détectées ... Cependant, avec madge , vous pouvez imprimer une liste des dépendances circulaires pour les approcher.

madge --circular --format AMD /path/src
15
Pascalius

Si vous n'avez pas besoin de charger vos dépendances au début (par exemple, lorsque vous étendez une classe), voici ce que vous pouvez faire: (tiré de http://requirejs.org/docs/api. html # circular )

Dans le fichier a.js:

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

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

    });

Et dans l'autre fichier 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();
            }
        };

    });

Dans l'exemple du PO, voici comment cela changerait:

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

Je voudrais juste éviter la dépendance circulaire. Peut-être quelque chose comme:

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

Je ne pense pas que ce soit une bonne idée de contourner ce problème et d'essayer de conserver la dépendance circulaire. On se sent juste comme une mauvaise pratique générale. Dans ce cas, cela peut fonctionner car vous avez réellement besoin de ces modules lorsque la fonction exportée est appelée. Mais imaginons le cas où des modules sont requis et utilisés dans les fonctions de définition proprement dites. Aucune solution de contournement ne fonctionnera. C'est probablement la raison pour laquelle require.js échoue rapidement lors de la détection de dépendance circulaire dans les dépendances de la fonction de définition.

Si vous devez vraiment ajouter une solution de contournement, la plus propre OMI nécessitera une dépendance juste à temps (dans les fonctions exportées dans ce cas), les fonctions de définition fonctionneront correctement. Mais même une OMI plus propre consiste simplement à éviter les dépendances circulaires, ce qui est très facile à faire dans votre cas.

5
Shu

J'ai examiné la documentation sur les dépendances circulaires: http://requirejs.org/docs/api.html#circular

S'il y a une dépendance circulaire avec a et b, il est dit dans votre module d'ajouter require en tant que dépendance dans votre module de la manière suivante:

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

alors quand vous avez besoin de "un" appelez simplement "un" comme ceci:

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

Cela a fonctionné pour moi

5
yeahdixon

Toutes les réponses postées (sauf https://stackoverflow.com/a/25170248/14731 ) sont incorrectes. Même la documentation officielle (à partir de novembre 2014) est fausse.

La seule solution qui a fonctionné pour moi consiste à déclarer un fichier "gatekeeper" et à le faire définir toute méthode qui dépend des dépendances circulaires. Voir https://stackoverflow.com/a/26809254/14731 pour un exemple concret.


Voici pourquoi les solutions ci-dessus ne fonctionneront pas.

  1. Vous ne pouvez pas:
var a;
require(['A'], function( A ){
     a = new A();
});

et ensuite, utiliser a ultérieurement, car rien ne garantit que ce bloc de code sera exécuté avant le bloc de code qui utilise a. (Cette solution est trompeuse car elle fonctionne 90% du temps)

  1. Je ne vois aucune raison de croire que exports ne soit pas vulnérable aux mêmes conditions de concurrence.

la solution à cela est:

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

    });

maintenant nous pouvons utiliser ces modules A et B dans le 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

    });
5
Gili

Dans mon cas, j'ai résolu la dépendance circulaire en déplaçant le code de l'objet "plus simple" dans l'objet plus complexe. Pour moi, c'était une collection et une classe de modèles. Je suppose que dans votre cas, j'ajouterais les parties de l'entreprise spécifiques aux employés dans la catégorie des employés.

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

Un peu hacky, mais cela devrait fonctionner pour des cas simples. Et si vous refactorisez addEmployee pour prendre un employé comme paramètre, la dépendance devrait être encore plus évidente pour les étrangers.

0
Björn Tantau