Was ist der richtige Weg, um zwischen Controllern in AngularJS zu kommunizieren?


473

Was ist der richtige Weg, um zwischen Controllern zu kommunizieren?

Ich benutze derzeit einen schrecklichen Fudge mit 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
Völlig umstritten, aber in Angular sollten Sie immer $ window anstelle des nativen JS-Fensterobjekts verwenden. Auf diese Weise können Sie es in Ihren Tests herausstechen :)
Dan M

1
Bitte beachten Sie den Kommentar in der Antwort unten von mir zu diesem Thema. $ Broadcast ist nicht länger teurer als $ emit. Siehe den jsperf-Link, auf den ich dort verwiesen habe.
Zumalifeguard

Antworten:


457

Bearbeiten : Das in dieser Antwort angesprochene Problem wurde in angle.js Version 1.2.7 behoben . $broadcastJetzt wird das Sprudeln über nicht registrierte Bereiche vermieden und läuft genauso schnell wie $ emit. $ Broadcast-Leistungen sind identisch mit $ emit mit Winkel 1.2.16

So, jetzt können Sie:

  • Verwendung $broadcastaus dem$rootScope
  • Hören Sie mit $on dem lokalen$scope , der über das Ereignis wissen muss

Originalantwort unten

Ich rate dringend, nicht $rootScope.$broadcast+ $scope.$on, sondern $rootScope.$emit+ zu verwenden $rootScope.$on. Ersteres kann schwerwiegende Leistungsprobleme verursachen, wie von @numan angesprochen. Das liegt daran, dass das Ereignis durch alle Bereiche sprudelt .

Letzteres (mit $rootScope.$emit+ $rootScope.$on) leidet jedoch nicht darunter und kann daher als schneller Kommunikationskanal verwendet werden!

Aus der Winkeldokumentation von $emit:

Versendet einen Ereignisnamen über die Bereichshierarchie nach oben und benachrichtigt den Registrierten

Da oben kein Bereich $rootScopevorhanden ist, tritt kein Sprudeln auf. Es ist absolut sicher, $rootScope.$emit()/ $rootScope.$on()als EventBus zu verwenden.

Es gibt jedoch ein Problem, wenn Sie es in Controllern verwenden. Wenn Sie direkt $rootScope.$on()von einem Controller aus eine Bindung herstellen, müssen Sie die Bindung selbst bereinigen, wenn Ihr lokaler $scopeServer zerstört wird. Dies liegt daran, dass Controller (im Gegensatz zu Diensten) über die Lebensdauer einer Anwendung mehrmals instanziiert werden können, was dazu führen würde, dass Bindungen summiert werden und überall Speicherlecks entstehen :)

Um austragen, hört nur auf $scope‚s $destroyEreignis und dann die Funktion aufrufen , die von zurückgegeben wurde $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);
        }
    ]);

Ich würde sagen, das ist nicht wirklich eine eckenspezifische Sache, da es auch für andere EventBus-Implementierungen gilt, dass Sie Ressourcen bereinigen müssen.

In diesen Fällen können Sie Ihnen jedoch das Leben erleichtern. Zum Beispiel könnten Sie einen Affen-Patch erstellen $rootScopeund ihm einen geben $onRootScope, der Ereignisse abonniert, die auf dem $rootScopeausgegeben werden, aber auch den Handler direkt bereinigt, wenn der lokale $scopeServer zerstört wird.

Der sauberste Weg, um Affen zu patchen $rootScope, um eine solche $onRootScopeMethode bereitzustellen , wäre durch einen Dekorateur (ein Run-Block wird es wahrscheinlich auch gut machen, aber pssst, sag es niemandem)

Um sicherzustellen, dass die $onRootScopeEigenschaft beim Aufzählen nicht unerwartet angezeigt wird, verwenden $scopewir Object.defineProperty()und setzen enumerableauf false. Denken Sie daran, dass Sie möglicherweise eine ES5-Unterlegscheibe benötigen.

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

Mit dieser Methode kann der Controller-Code von oben vereinfacht werden, um:

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

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

Als Endergebnis all dessen rate ich Ihnen dringend, $rootScope.$emit+ zu verwenden $scope.$onRootScope.

Übrigens versuche ich, das Winkel-Team davon zu überzeugen, das Problem innerhalb des Winkelkerns anzugehen. Hier findet eine Diskussion statt: https://github.com/angular/angular.js/issues/4574

Hier ist ein jsperf, das zeigt, wie viel Perfektion $broadcastin einem anständigen Szenario mit nur 100 Sekunden auf den Tisch kommt $scope.

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

jsperf Ergebnisse


Ich versuche, Ihre zweite Option auszuführen, erhalte jedoch eine Fehlermeldung: Nicht erfasst TypeError: Eigenschaft: $ onRootScope kann nicht neu definiert werden, genau dort, wo ich die Object.defineProperty ausführe ....
Scott

Vielleicht habe ich etwas vermasselt, als ich es hier eingefügt habe. Ich benutze es in der Produktion und es funktioniert großartig. Ich werde morgen einen Blick darauf werfen :)
Christoph

@Scott Ich habe es eingefügt, aber der Code war bereits korrekt und ist genau das, was wir in der Produktion verwenden. Können Sie überprüfen, ob Ihre Website keinen Tippfehler enthält? Kann ich Ihren Code irgendwo sehen, um bei der Fehlerbehebung zu helfen?
Christoph

@Christoph gibt es eine gute Möglichkeit, den Dekorator in IE8 auszuführen, da Object.defineProperty für Nicht-DOM-Objekte nicht unterstützt wird?
Joshschreuder

59
Dies war eine sehr clevere Lösung für das Problem, die jedoch nicht mehr benötigt wird. In der neuesten Version von Angular (1.2.16) und wahrscheinlich früher wurde dieses Problem behoben. Jetzt wird $ Broadcast nicht ohne Grund jeden Nachkommen-Controller besuchen. Es werden nur diejenigen besucht, die tatsächlich auf die Veranstaltung hören. Ich habe den oben genannten jsperf aktualisiert, um zu demonstrieren, dass das Problem jetzt behoben ist: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

107

Die beste Antwort hier war eine Umgehung eines Angular-Problems, das nicht mehr existiert (zumindest in Versionen> 1.2.16 und "wahrscheinlich früher"), wie @zumalifeguard erwähnt hat. Aber ich lese all diese Antworten ohne eine tatsächliche Lösung.

Es scheint mir, dass die Antwort jetzt sein sollte

  • Verwendung $broadcastaus dem$rootScope
  • Hören Sie mit $on dem lokalen$scope , der über das Ereignis wissen muss

Also zu veröffentlichen

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

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

}]);

Und abonnieren

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

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

}]);

Plunker

Wenn Sie den Listener lokal registrieren $scope, wird er automatisch von $destroyselbst zerstört, wenn der zugehörige Controller entfernt wird.


1
Wissen Sie, ob dasselbe Muster mit der controllerAsSyntax verwendet werden kann? Ich konnte $rootScopeim Abonnenten das Ereignis abhören, war aber nur neugierig, ob es ein anderes Muster gab.
Kanten

3
@edhedges Ich denke, Sie könnten das $scopeexplizit injizieren . John Papa schreibt über Ereignisse, die eine "Ausnahme" von seiner üblichen Regel darstellen, $scopesich von seinen Controllern fernzuhalten (ich verwende Anführungszeichen, weil sie, wie er immer Controller Asnoch erwähnt $scope, direkt unter der Motorhaube liegen).
vornehmsten

Mit unter der Motorhaube meinen Sie, dass Sie immer noch per Injektion darauf zugreifen können?
Kanten

2
@edhedges Ich habe meine Antwort controller aswie gewünscht mit einer Syntaxalternative aktualisiert . Ich hoffe das hast du gemeint.
vornehmsten

3
@dsdsdsdsd, Dienste / Fabriken / Anbieter bleiben für immer da. In einer Angular-App gibt es immer nur einen (Singletons). Controller hingegen sind an Funktionen gebunden: Komponenten / Direktiven / ng-Controller, die wiederholt werden können (wie Objekte aus einer Klasse) und bei Bedarf kommen und gehen. Warum soll ein Steuerelement und sein Controller vorhanden bleiben, wenn Sie es nicht mehr benötigen? Das ist genau die Definition eines Speicherverlusts.
vornehmsten


42

Da defineProperty ein Problem mit der Browserkompatibilität aufweist, können wir über die Verwendung eines Dienstes nachdenken.

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

und verwenden Sie es in Controller wie folgt:

  • Controller 1

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

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

7
+1 für die automatische Abmeldung, wenn der Bereich zerstört wird.
Federico Nafria

6
Ich mag diese Lösung. 2 Änderungen, die ich vorgenommen habe: (1) Ermöglichen Sie dem Benutzer, 'Daten' an die Emit-Nachricht zu übergeben. (2) Machen Sie die Übergabe von 'Bereich' optional, damit dies sowohl in Singleton-Diensten als auch in Controllern verwendet werden kann. Sie können diese Änderungen hier implementiert sehen: gist.github.com/turtlemonvh/10686980/…
turtlemonvh


15

Die tatsächliche Verwendung von Emit und Broadcast ist ineffizient, da das Ereignis in der Bereichshierarchie auf und ab sprudelt, was für eine komplexe Anwendung leicht zu einer Leistungsminderung führen kann.

Ich würde vorschlagen, einen Dienst zu nutzen. So habe ich es kürzlich in einem meiner Projekte implementiert - https://gist.github.com/3384419 .

Grundidee - Registrieren Sie einen Pubsub / Event-Bus als Service. Fügen Sie dann diesen Eventbus ein, wo immer Sie Events / Themen abonnieren oder veröffentlichen müssen.


7
Und wenn ein Controller nicht mehr benötigt wird, wie können Sie ihn automatisch abbestellen? Wenn Sie dies nicht tun, wird der Controller aufgrund des Schließens niemals aus dem Speicher entfernt, und Sie erkennen weiterhin Nachrichten an ihn. Um dies zu vermeiden, müssen Sie es dann manuell entfernen. Die Verwendung von $ wird nicht ausgeführt.
Renan Tomal Fernandes

1
Das ist ein fairer Punkt. Ich denke, es kann dadurch gelöst werden, wie Sie Ihre Anwendung entwerfen. In meinem Fall habe ich eine App mit nur einer Seite, daher ist das Problem leichter zu lösen. Trotzdem denke ich, dass dies viel sauberer wäre, wenn Angular Komponenten-Lifecycle-Hooks hätte, an denen Sie solche Dinge verdrahten / verdrahten könnten.
numan salati

6
Ich lasse das einfach hier, wie es noch niemand gesagt hat. Die Verwendung von rootScope als EventBus ist nicht ineffizient, da $rootScope.$emit()nur Blasen nach oben blasen. Da es jedoch keinen Spielraum darüber $rootScopegibt, gibt es nichts zu befürchten. Wenn Sie also nur verwenden $rootScope.$emit()und $rootScope.$on()einen schnellen systemweiten EventBus haben.
Christoph

1
Das einzige, was Sie beachten müssen, ist, dass Sie bei Verwendung $rootScope.$on()in Ihrem Controller die Ereignisbindung bereinigen müssen, da sie sich sonst summieren, wenn jedes Mal, wenn der Controller instanziiert wird, eine neue erstellt wird und dies nicht der Fall ist werden automatisch für Sie zerstört, da Sie $rootScopedirekt an binden .
Christoph

In der neuesten Version von Angular (1.2.16) und wahrscheinlich früher wurde dieses Problem behoben. Jetzt wird $ Broadcast nicht ohne Grund jeden Nachkommen-Controller besuchen. Es werden nur diejenigen besucht, die tatsächlich auf die Veranstaltung hören. Ich habe den oben genannten jsperf aktualisiert, um zu demonstrieren, dass das Problem jetzt behoben ist: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

14

Mit get- und set-Methoden innerhalb eines Dienstes können Sie sehr einfach Nachrichten zwischen Controllern übertragen.

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

In HTML HTML können Sie dies überprüfen

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

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

+1 Eine dieser offensichtlichen Methoden - sobald Sie es erfahren haben - ausgezeichnet! Ich habe eine allgemeinere Version mit den Methoden set (Schlüssel, Wert) und get (Schlüssel) implementiert - eine nützliche Alternative zu $ ​​Broadcast.
TonyWilk

8

In Bezug auf den ursprünglichen Code möchten Sie anscheinend Daten zwischen Bereichen austauschen. Um entweder Daten oder Status zwischen $ scope zu teilen, schlagen die Dokumente die Verwendung eines Dienstes vor:

  • So führen Sie zustandslosen oder statusbehafteten Code aus, der von Controllern gemeinsam genutzt wird - Verwenden Sie stattdessen Winkeldienste.
  • Instanziieren oder Verwalten des Lebenszyklus anderer Komponenten (z. B. Erstellen von Dienstinstanzen).

Ref: Angular Docs Link hier


5

Ich habe tatsächlich begonnen, Postal.js als Nachrichtenbus zwischen Controllern zu verwenden.

Als Nachrichtenbus bietet er viele Vorteile, z. B. Bindungen im AMQP-Stil, die Art und Weise, wie Post W / iFrames und Web-Sockets integrieren kann, und vieles mehr.

Ich habe einen Dekorateur benutzt, um die Post einzurichten $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;
    }]);
});

Hier ist ein Link zu einem Blog-Beitrag zum Thema ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/


3

So mache ich das mit Factory / Services und Simple Dependency Injection (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
Ihre beiden Controller kommunizieren nicht, sie verwenden nur denselben Dienst. Das ist nicht dasselbe.
Greg

@ Greg Sie können dasselbe mit weniger Code erreichen, indem Sie einen gemeinsamen Dienst haben und bei Bedarf $ watchs hinzufügen.
Capaj

3

Mir hat gefallen, wie $rootscope.emitdie Interkommunikation erreicht wurde. Ich schlage die saubere und leistungsfähige Lösung vor, ohne den globalen Raum zu verschmutzen.

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

Bei Speicherverlust sollten Sie eine zusätzliche Methode hinzufügen, um die Registrierung von den Ereignis-Listenern aufzuheben. Wie auch immer, gute triviale Probe
Raffaeu

2

Hier ist der schnelle und schmutzige Weg.

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

    };

}

Hier ist eine Beispielfunktion, die in einem der Geschwister-Controller hinzugefügt werden kann:

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

    alert('Sink that ship!');                       

});

und hier ist natürlich dein HTML:

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

16
Warum spritzt du nicht einfach $rootScope?
Pieter Herroelen

1

Starten Sie Winkel 1.5 und seinen komponentenbasierten Entwicklungsfokus. Die empfohlene Art der Interaktion von Komponenten besteht in der Verwendung der Eigenschaft 'require' und in Eigenschaftsbindungen (Eingabe / Ausgabe).

Eine Komponente würde eine andere Komponente (z. B. die Stammkomponente) benötigen und einen Verweis auf ihren Controller erhalten:

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

Sie können dann die Methoden der Stammkomponente in Ihrer untergeordneten Komponente verwenden:

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

Dies ist die Root-Komponenten-Controller-Funktion:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Hier ist eine vollständige architektonische Übersicht: Komponentenkommunikation


0

Sie können auf diese Hallo-Funktion überall im Modul zugreifen

Controller eins

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

zweite Steuerung

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

Mehr Infos hier


7
Ein bisschen spät zur Party, aber: Tu das nicht. Das Einfügen einer Funktion in den Stammbereich entspricht dem Erstellen einer globalen Funktion, die alle möglichen Probleme verursachen kann.
Dan Pantry

0

Ich werde einen Dienst erstellen und eine Benachrichtigung verwenden.

  1. Erstellen Sie eine Methode im Benachrichtigungsdienst
  2. Erstellen Sie eine generische Methode zum Senden von Benachrichtigungen im Benachrichtigungsdienst.
  3. Rufen Sie vom Quellcontroller die notificationService.Method auf. Ich übergebe auch das entsprechende Objekt, um es bei Bedarf beizubehalten.
  4. Innerhalb der Methode speichere ich Daten im Benachrichtigungsdienst und rufe die generische Benachrichtigungsmethode auf.
  5. Im Ziel-Controller lausche ich ($ scope.on) auf das Broadcast-Ereignis und greife auf Daten vom Notification Service zu.

Da der Benachrichtigungsdienst zu jedem Zeitpunkt ein Singleton ist, sollte er in der Lage sein, persistente Daten für alle bereitzustellen.

Hoffe das hilft


0

Sie können den integrierten AngularJS-Dienst verwenden $rootScope und diesen Dienst in beide Controller einfügen. Sie können dann auf Ereignisse warten, die für das $ rootScope-Objekt ausgelöst werden.

$ rootScope bietet zwei aufgerufene Ereignis-Dispatcher, $emit and $broadcastdie für das Versenden von Ereignissen verantwortlich sind (möglicherweise benutzerdefinierte Ereignisse) und die $rootScope.$onFunktion zum Hinzufügen eines Ereignis-Listeners verwenden.


0

Sie sollten den Dienst verwenden, da der $rootscopeZugriff von der gesamten Anwendung aus erfolgt und die Last erhöht wird, oder Sie können die Rootparams verwenden, wenn Ihre Daten nicht mehr sind.


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

Sie können dies tun, indem Sie Winkelereignisse verwenden, die $ emit und $ Broadcast sind. Nach unserem Kenntnisstand ist dies der beste, effizienteste und effektivste Weg.

Zuerst rufen wir eine Funktion von einem Controller aus auf.

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

    });
});

Sie können auch $ rootScope anstelle von $ scope verwenden. Verwenden Sie Ihren Controller entsprechend.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.