Quelle est la bonne façon de communiquer entre les contrôleurs dans AngularJS?


473

Quelle est la bonne façon de communiquer entre les contrôleurs?

J'utilise actuellement un horrible fudge impliquant window:

function StockSubgroupCtrl($scope, $http) {
    $scope.subgroups = [];
    $scope.handleSubgroupsLoaded = function(data, status) {
        $scope.subgroups = data;
    }
    $scope.fetch = function(prod_grp) {
        $http.get('/api/stock/groups/' + prod_grp + '/subgroups/').success($scope.handleSubgroupsLoaded);
    }
    window.fetchStockSubgroups = $scope.fetch;
}

function StockGroupCtrl($scope, $http) {
    ...
    $scope.select = function(prod_grp) {
        $scope.selectedGroup = prod_grp;
        window.fetchStockSubgroups(prod_grp);
    }
}

36
Totalement théorique, mais dans Angular, vous devez toujours utiliser $ window au lieu de l'objet de fenêtre JS natif. De cette façon, vous pouvez le cacher dans vos tests :)
Dan M

1
S'il vous plaît voir le commentaire dans la réponse ci-dessous de moi concernant ce problème. $ broadcast n'est plus plus cher que $ emit. Voir le lien jsperf auquel j'ai fait référence.
zumalifeguard

Réponses:


457

Edit : Le problème abordé dans cette réponse a été résolu dans angular.js version 1.2.7 . $broadcastévite désormais de se propager sur des étendues non enregistrées et s'exécute aussi vite que $ emit. Les performances $ broadcast sont identiques à $ emit avec angular 1.2.16

Vous pouvez donc désormais:

  • utiliser à $broadcastpartir du$rootScope
  • écouter en utilisant $on le local$scope qui a besoin de connaître l'événement

Réponse originale ci-dessous

Je conseille fortement de ne pas utiliser $rootScope.$broadcast+ $scope.$onmais plutôt $rootScope.$emit+ $rootScope.$on. Le premier peut entraîner de graves problèmes de performances, comme l'a soulevé @numan. En effet, l'événement bouillonnera dans tous les domaines.

Cependant, ce dernier (en utilisant $rootScope.$emit+ $rootScope.$on) n'en souffre pas et peut donc être utilisé comme canal de communication rapide!

De la documentation angulaire de $emit:

Distribue un nom d'événement vers le haut à travers la hiérarchie d'étendue notifiant les inscrits

Puisqu'il n'y a pas de portée ci $rootScope- dessus , il n'y a pas de bouillonnement. Il est totalement sûr d'utiliser $rootScope.$emit()/ $rootScope.$on()comme EventBus.

Cependant, il existe un problème lors de son utilisation à partir de contrôleurs. Si vous vous connectez directement à $rootScope.$on()partir d'un contrôleur, vous devrez nettoyer la liaison vous-même lorsque votre section locale sera $scopedétruite. En effet, les contrôleurs (contrairement aux services) peuvent être instanciés plusieurs fois au cours de la durée de vie d'une application, ce qui entraînerait la synthèse des liaisons, créant éventuellement des fuites de mémoire partout :)

Pour annuler l' enregistrement, il suffit d' écouter sur votre $scopede » $destroyl'événement, puis appeler la fonction qui a été renvoyée par $rootScope.$on.

angular
    .module('MyApp')
    .controller('MyController', ['$scope', '$rootScope', function MyController($scope, $rootScope) {

            var unbind = $rootScope.$on('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });

            $scope.$on('$destroy', unbind);
        }
    ]);

Je dirais que ce n'est pas vraiment une chose spécifique angulaire, car cela s'applique également aux autres implémentations EventBus, que vous devez nettoyer les ressources.

Cependant, vous pouvez vous faciliter la vie dans ces cas. Par exemple, vous pouvez corriger le singe $rootScopeet lui donner un $onRootScopequi s'abonne aux événements émis sur le $rootScopemais nettoie également directement le gestionnaire lorsque le local $scopeest détruit.

La façon la plus propre de patcher le singe $rootScopepour fournir une telle $onRootScopeméthode serait de passer par un décorateur (un bloc d'exécution le fera probablement très bien aussi mais pssst, ne le dites à personne)

Pour vous assurer que la $onRootScopepropriété ne s'affiche pas de manière inattendue lors de l'énumération, $scopenous utilisons Object.defineProperty()et définissons enumerablesur false. Gardez à l'esprit que vous pourriez avoir besoin d'une cale ES5.

angular
    .module('MyApp')
    .config(['$provide', function($provide){
        $provide.decorator('$rootScope', ['$delegate', function($delegate){

            Object.defineProperty($delegate.constructor.prototype, '$onRootScope', {
                value: function(name, listener){
                    var unsubscribe = $delegate.$on(name, listener);
                    this.$on('$destroy', unsubscribe);

                    return unsubscribe;
                },
                enumerable: false
            });


            return $delegate;
        }]);
    }]);

Avec cette méthode en place, le code du contrôleur ci-dessus peut être simplifié pour:

angular
    .module('MyApp')
    .controller('MyController', ['$scope', function MyController($scope) {

            $scope.$onRootScope('someComponent.someCrazyEvent', function(){
                console.log('foo');
            });
        }
    ]);

Donc, comme résultat final de tout cela, je vous conseille fortement d'utiliser $rootScope.$emit+ $scope.$onRootScope.

Btw, j'essaie de convaincre l'équipe angulaire de résoudre le problème dans le noyau angulaire. Il y a une discussion en cours ici: https://github.com/angular/angular.js/issues/4574

Voici un jsperf qui montre combien un impact de perf $broadcastapporte à la table dans un scénario décent avec seulement 100 $scope.

http://jsperf.com/rootscope-emit-vs-rootscope-broadcast

résultats jsperf


J'essaie de faire votre 2e option, mais j'obtiens une erreur: TypeError non capturé: Impossible de redéfinir la propriété: $ onRootScope là où je fais Object.defineProperty ....
Scott

Peut-être que j'ai foiré quelque chose quand je l'ai collé ici. Je l'utilise en production et ça marche très bien. Je jetterai un coup d'œil demain :)
Christoph

@Scott Je l'ai collé mais le code était déjà correct et correspond exactement à ce que nous utilisons en production. Pouvez-vous vérifier que vous n'avez pas de faute de frappe sur votre site? Puis-je voir votre code quelque part pour aider au dépannage?
Christoph

@Christoph est-il un bon moyen de faire le décorateur dans IE8, car il ne prend pas en charge Object.defineProperty sur les objets non DOM?
joshschreuder

59
C'était une solution très intelligente au problème, mais elle n'est plus nécessaire. La dernière version d'Angular (1.2.16), et probablement antérieure, a ce problème résolu. Maintenant, $ broadcast ne visitera pas tous les contrôleurs descendants sans raison. Il ne rendra visite qu'à ceux qui écoutent réellement l'événement. J'ai mis à jour le jsperf référencé ci-dessus pour démontrer que le problème est désormais résolu: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

107

La meilleure réponse ici était un contournement d'un problème angulaire qui n'existe plus (au moins dans les versions> 1.2.16 et "probablement plus tôt") comme @zumalifeguard l'a mentionné. Mais il me reste à lire toutes ces réponses sans solution réelle.

Il me semble que la réponse devrait maintenant être

  • utiliser à $broadcastpartir du$rootScope
  • écouter en utilisant $on le local$scope qui a besoin de connaître l'événement

Donc pour publier

// EXAMPLE PUBLISHER
angular.module('test').controller('CtrlPublish', ['$rootScope', '$scope',
function ($rootScope, $scope) {

  $rootScope.$broadcast('topic', 'message');

}]);

Et abonnez-vous

// EXAMPLE SUBSCRIBER
angular.module('test').controller('ctrlSubscribe', ['$scope',
function ($scope) {

  $scope.$on('topic', function (event, arg) { 
    $scope.receiver = 'got your ' + arg;
  });

}]);

Plunkers

Si vous enregistrez l'auditeur sur le plan local $scope, il sera automatiquement détruit par $destroylui - même quand on retire le contrôleur associé.


1
Savez-vous si ce même modèle peut être utilisé avec la controllerAssyntaxe? J'ai pu utiliser $rootScopel'abonné pour écouter l'événement, mais j'étais simplement curieux de savoir s'il y avait un modèle différent.
edhedges

3
@edhedges Je suppose que vous pourriez injecter $scopeexplicitement. John Papa écrit que les événements sont une "exception" à sa règle habituelle de garder $scope"à distance " ses contrôleurs (j'utilise des guillemets car comme il le mentionne Controller Astoujours $scope, c'est juste sous le capot).
2014 le plus chic

Par sous le capot, voulez-vous dire que vous pouvez toujours y accéder par injection?
edhedges

2
@edhedges J'ai mis à jour ma réponse avec une controller asalternative de syntaxe comme demandé. J'espère que c'est ce que tu voulais dire.
Poshest

3
@dsdsdsdsd, les services / usines / fournisseurs resteront pour toujours. Il y a toujours un et un seul (singletons) dans une application Angular. Les contrôleurs, d'autre part, sont liés à la fonctionnalité: composants / directives / ng-controller, qui peuvent être répétés (comme des objets fabriqués à partir d'une classe) et ils vont et viennent si nécessaire. Pourquoi voulez-vous qu'un contrôle et son contrôleur continuent d'exister alors que vous n'en avez plus besoin? C'est la définition même d'une fuite de mémoire.
Poshest


42

Étant donné que defineProperty a un problème de compatibilité avec le navigateur, je pense que nous pouvons penser à utiliser un service.

angular.module('myservice', [], function($provide) {
    $provide.factory('msgBus', ['$rootScope', function($rootScope) {
        var msgBus = {};
        msgBus.emitMsg = function(msg) {
        $rootScope.$emit(msg);
        };
        msgBus.onMsg = function(msg, scope, func) {
            var unbind = $rootScope.$on(msg, func);
            scope.$on('$destroy', unbind);
        };
        return msgBus;
    }]);
});

et l'utiliser dans un contrôleur comme celui-ci:

  • contrôleur 1

    function($scope, msgBus) {
        $scope.sendmsg = function() {
            msgBus.emitMsg('somemsg')
        }
    }
    
  • contrôleur 2

    function($scope, msgBus) {
        msgBus.onMsg('somemsg', $scope, function() {
            // your logic
        });
    }
    

7
+1 pour la désinscription automatique lorsque l'étendue est détruite.
Federico Nafria

6
J'aime cette solution. 2 modifications que j'ai apportées: (1) permettre à l'utilisateur de transmettre des «données» au message d'émission (2) rendre le passage de «portée» facultatif afin que cela puisse être utilisé dans les services singleton ainsi que les contrôleurs. Vous pouvez voir ces changements implémentés ici: gist.github.com/turtlemonvh/10686980/…
turtlemonvh


15

En fait, utiliser émettre et diffuser est inefficace car l'événement bouillonne de haut en bas dans la hiérarchie de la portée, ce qui peut facilement se dégrader en réduction des performances pour une application complexe.

Je suggère d'utiliser un service. Voici comment je l'ai récemment implémenté dans l'un de mes projets - https://gist.github.com/3384419 .

Idée de base - enregistrer un pubsub / bus d'événements en tant que service. Injectez ensuite cet Eventbus partout où vous devez vous abonner ou publier des événements / sujets.


7
Et lorsqu'un contrôleur n'est plus nécessaire, comment le désinscrire automatiquement? Si vous ne le faites pas, en raison de la fermeture, le contrôleur ne sera jamais supprimé de la mémoire et vous y sentirez toujours des messages. Pour éviter cela, vous devrez le supprimer puis manuellement. L'utilisation de $ sur ceci ne se produira pas.
Renan Tomal Fernandes

1
c'est un bon point. Je pense que cela peut être résolu par la façon dont vous concevez votre application. dans mon cas, j'ai une application d'une seule page, donc c'est un problème plus gérable. Cela dit, je pense que ce serait beaucoup plus propre si angulaire avait des crochets de cycle de vie des composants où vous pourriez câbler / déconnecter des choses comme ça.
numan salati

6
Je laisse juste cela ici comme personne ne l'avait dit auparavant. L'utilisation du rootScope comme un EventBus n'est pas inefficace car $rootScope.$emit()seules des bulles montent. Cependant, comme il n'y a pas de portée au-dessus, $rootScopeil n'y a rien à craindre. Donc, si vous utilisez juste $rootScope.$emit()et $rootScope.$on()vous aurez un EventBus rapide à l'échelle du système.
Christoph

1
La seule chose dont vous devez être conscient est que si vous utilisez à l' $rootScope.$on()intérieur de votre contrôleur, vous devrez nettoyer la liaison d'événement, sinon ils résumeront car il en crée un nouveau chaque fois que le contrôleur est instancié et ils ne le font pas être automatiquement détruit pour vous puisque vous vous liez $rootScopedirectement à.
Christoph

La dernière version d'Angular (1.2.16), et probablement antérieure, a ce problème résolu. Maintenant, $ broadcast ne visitera pas tous les contrôleurs descendants sans raison. Il ne rendra visite qu'à ceux qui écoutent réellement l'événement. J'ai mis à jour le jsperf référencé ci-dessus pour démontrer que le problème est désormais résolu: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

14

En utilisant les méthodes get et set dans un service, vous pouvez très facilement passer des messages entre les contrôleurs.

var myApp = angular.module("myApp",[]);

myApp.factory('myFactoryService',function(){


    var data="";

    return{
        setData:function(str){
            data = str;
        },

        getData:function(){
            return data;
        }
    }


})


myApp.controller('FirstController',function($scope,myFactoryService){
    myFactoryService.setData("Im am set in first controller");
});



myApp.controller('SecondController',function($scope,myFactoryService){
    $scope.rslt = myFactoryService.getData();
});

en HTML HTML, vous pouvez vérifier comme ça

<div ng-controller='FirstController'>  
</div>

<div ng-controller='SecondController'>
    {{rslt}}
</div>

+1 Une de ces méthodes évidentes une fois qu'on vous l'a dit - excellente! J'ai implémenté une version plus générale avec les méthodes set (clé, valeur) et get (clé) - une alternative utile à $ broadcast.
TonyWilk

8

En ce qui concerne le code d'origine - il semble que vous souhaitiez partager des données entre les étendues. Pour partager des données ou un état entre $ scope, les documents suggèrent d'utiliser un service:

  • Pour exécuter du code sans état ou avec état partagé entre plusieurs contrôleurs - Utilisez plutôt des services angulaires.
  • Pour instancier ou gérer le cycle de vie d'autres composants (par exemple, pour créer des instances de service).

Ref: lien Angular Docs ici


5

J'ai en fait commencé à utiliser Postal.js comme bus de messages entre les contrôleurs.

Il présente de nombreux avantages en tant que bus de messages, comme les liaisons de style AMQP, la façon dont la poste peut intégrer des iFrames et des sockets Web, et bien d'autres choses.

J'ai utilisé un décorateur pour installer Postal sur $scope.$bus...

angular.module('MyApp')  
.config(function ($provide) {
    $provide.decorator('$rootScope', ['$delegate', function ($delegate) {
        Object.defineProperty($delegate.constructor.prototype, '$bus', {
            get: function() {
                var self = this;

                return {
                    subscribe: function() {
                        var sub = postal.subscribe.apply(postal, arguments);

                        self.$on('$destroy',
                        function() {
                            sub.unsubscribe();
                        });
                    },
                    channel: postal.channel,
                    publish: postal.publish
                };
            },
            enumerable: false
        });

        return $delegate;
    }]);
});

Voici un lien vers un article de blog sur le sujet ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/


3

C'est ainsi que je le fais avec Factory / Services et l' injection de dépendance simple (DI) .

myApp = angular.module('myApp', [])

# PeopleService holds the "data".
angular.module('myApp').factory 'PeopleService', ()->
  [
    {name: "Jack"}
  ]

# Controller where PeopleService is injected
angular.module('myApp').controller 'PersonFormCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
  $scope.people = PeopleService
  $scope.person = {} 

  $scope.add = (person)->
    # Simply push some data to service
    PeopleService.push angular.copy(person)
]

# ... and again consume it in another controller somewhere...
angular.module('myApp').controller 'PeopleListCtrl', ['$scope','PeopleService', ($scope, PeopleService)->
  $scope.people = PeopleService
]

1
Vos deux contrôleurs ne communiquent pas, ils n'utilisent qu'un seul et même service. Ce n'est pas la même chose.
Greg

@Greg vous pouvez réaliser la même chose avec moins de code en ayant un service partagé et en ajoutant $ watch si nécessaire.
Capaj

3

J'ai aimé la façon dont $rootscope.emitétait utilisé pour réaliser l'intercommunication. Je propose la solution propre et performante sans polluer l'espace global.

module.factory("eventBus",function (){
    var obj = {};
    obj.handlers = {};
    obj.registerEvent = function (eventName,handler){
        if(typeof this.handlers[eventName] == 'undefined'){
        this.handlers[eventName] = [];  
    }       
    this.handlers[eventName].push(handler);
    }
    obj.fireEvent = function (eventName,objData){
       if(this.handlers[eventName]){
           for(var i=0;i<this.handlers[eventName].length;i++){
                this.handlers[eventName][i](objData);
           }

       }
    }
    return obj;
})

//Usage:

//In controller 1 write:
eventBus.registerEvent('fakeEvent',handler)
function handler(data){
      alert(data);
}

//In controller 2 write:
eventBus.fireEvent('fakeEvent','fakeData');

Pour les fuites de mémoire, vous devez ajouter une méthode supplémentaire pour vous désinscrire des écouteurs d'événements. Quoi qu'il en soit bon échantillon trivial
Raffaeu

2

Voici le moyen rapide et sale.

// Add $injector as a parameter for your controller

function myAngularController($scope,$injector){

    $scope.sendorders = function(){

       // now you can use $injector to get the 
       // handle of $rootScope and broadcast to all

       $injector.get('$rootScope').$broadcast('sinkallships');

    };

}

Voici un exemple de fonction à ajouter dans l'un des contrôleurs frères:

$scope.$on('sinkallships', function() {

    alert('Sink that ship!');                       

});

et bien sûr voici votre HTML:

<button ngclick="sendorders()">Sink Enemy Ships</button>

16
Pourquoi tu n'injectes pas $rootScope?
Pieter Herroelen

1

Démarrage d'angular 1.5 et de son développement basé sur les composants. La manière recommandée pour les composants d'interagir est d'utiliser la propriété 'require' et les liaisons de propriété (entrée / sortie).

Un composant nécessiterait un autre composant (par exemple le composant racine) et obtiendrait une référence à son contrôleur:

angular.module('app').component('book', {
    bindings: {},
    require: {api: '^app'},
    template: 'Product page of the book: ES6 - The Essentials',
    controller: controller
});

Vous pouvez ensuite utiliser les méthodes du composant racine dans votre composant enfant:

$ctrl.api.addWatchedBook('ES6 - The Essentials');

Il s'agit de la fonction de contrôleur de composant racine:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Voici un aperçu architectural complet: Communications de composants


0

Vous pouvez accéder à cette fonction bonjour n'importe où dans le module

Contrôleur un

 $scope.save = function() {
    $scope.hello();
  }

deuxième contrôleur

  $rootScope.hello = function() {
    console.log('hello');
  }

Plus d'infos ici


7
Un peu tard pour la fête mais: ne fais pas ça. Mettre une fonction sur la portée racine revient à rendre une fonction globale, ce qui peut provoquer toutes sortes de problèmes.
Dan Pantry

0

Je vais créer un service et utiliser la notification.

  1. Créer une méthode dans le service de notification
  2. Créez une méthode générique pour diffuser une notification dans Notification Service.
  3. Depuis le contrôleur source, appelez notificationService.Method. Je passe également l'objet correspondant pour persister si besoin.
  4. Dans la méthode, je conserve les données dans le service de notification et j'appelle la méthode de notification générique.
  5. Dans le contrôleur de destination, j'écoute ($ scope.on) l'événement de diffusion et j'accède aux données du service de notification.

Comme à tout moment le service de notification est singleton, il devrait être en mesure de fournir des données persistantes à travers.

J'espère que cela t'aides


0

Vous pouvez utiliser le $rootScopeservice intégré AngularJS et injecter ce service dans vos deux contrôleurs. Vous pouvez ensuite écouter les événements déclenchés sur l'objet $ rootScope.

$ rootScope fournit deux répartiteurs d'événements appelés $emit and $broadcastqui sont responsables de la répartition des événements (peuvent être des événements personnalisés) et utilisent la $rootScope.$onfonction pour ajouter un écouteur d'événements.


0

Vous devez utiliser le Service, car l' $rootscopeaccès à partir de toute l'application, et cela augmente la charge, ou vous pouvez utiliser les rootparams si vos données ne sont pas plus.


0
function mySrvc() {
  var callback = function() {

  }
  return {
    onSaveClick: function(fn) {
      callback = fn;
    },
    fireSaveClick: function(data) {
      callback(data);
    }
  }
}

function controllerA($scope, mySrvc) {
  mySrvc.onSaveClick(function(data) {
    console.log(data)
  })
}

function controllerB($scope, mySrvc) {
  mySrvc.fireSaveClick(data);
}

0

Vous pouvez le faire en utilisant des événements angulaires qui sont $ emit et $ broadcast. Selon nos connaissances, c'est le meilleur moyen, efficace et efficient.

D'abord, nous appelons une fonction d'un contrôleur.

var myApp = angular.module('sample', []);
myApp.controller('firstCtrl', function($scope) {
    $scope.sum = function() {
        $scope.$emit('sumTwoNumber', [1, 2]);
    };
});
myApp.controller('secondCtrl', function($scope) {
    $scope.$on('sumTwoNumber', function(e, data) {
        var sum = 0;
        for (var a = 0; a < data.length; a++) {
            sum = sum + data[a];
        }
        console.log('event working', sum);

    });
});

Vous pouvez également utiliser $ rootScope à la place de $ scope. Utilisez votre contrôleur en conséquence.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.