Qual è il modo corretto di comunicare tra i controller in AngularJS?


473

Qual è il modo corretto di comunicare tra i controller?

Attualmente sto usando un orribile fondente che coinvolge 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
Totalmente discutibile, ma in Angular, dovresti sempre usare $ window invece dell'oggetto nativo della finestra JS. In questo modo puoi cancellarlo nei tuoi test :)
Dan M,

1
Si prega di vedere il commento nella risposta di seguito da me in merito a questo problema. $ broadcast non è più costoso di $ emit. Vedi il link jsperf a cui ho fatto riferimento qui.
zumalifeguard

Risposte:


457

Modifica : il problema risolto in questa risposta è stato risolto in angular.js versione 1.2.7 . $broadcastora evita il gorgogliamento degli ambiti non registrati e funziona alla stessa velocità di $ emit. Le prestazioni di $ broadcast sono identiche a $ emit con l'angolo 1.2.16

Quindi ora puoi:

  • utilizzare $broadcastdal$rootScope
  • ascolta usando $on dal locale$scope che deve conoscere l'evento

Risposta originale di seguito

Consiglio vivamente di non usare $rootScope.$broadcast+ $scope.$onma piuttosto $rootScope.$emit+ $rootScope.$on. Il primo può causare seri problemi di prestazioni sollevati da @numan. Questo perché l'evento passerà attraverso tutti gli ambiti.

Tuttavia, quest'ultimo (usando $rootScope.$emit+ $rootScope.$on) non ne soffre e può quindi essere usato come canale di comunicazione veloce!

Dalla documentazione angolare di $emit:

Invia un nome di evento verso l'alto attraverso la gerarchia dell'ambito che notifica al registrato

Dal momento che non esiste alcun campo di applicazione sopra $rootScope, non si verificano bolle. È totalmente sicuro da usare $rootScope.$emit()/ $rootScope.$on()come EventBus.

Tuttavia, c'è un gotcha quando lo si utilizza da Controller. Se esegui il binding direttamente $rootScope.$on()da un controller, dovrai ripulire il binding da solo quando sei locale$scope viene distrutto. Questo perché i controller (a differenza dei servizi) possono essere istanziati più volte nel corso della vita di un'applicazione, il che si tradurrebbe in riassunti di associazioni che alla fine creano perdite di memoria ovunque :)

Per annullare la registrazione, basta ascoltare sul vostro $scope's $destroyevento e quindi chiamare la funzione che è stato restituito da $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);
        }
    ]);

Direi che non è una cosa davvero angolare in quanto si applica anche ad altre implementazioni EventBus, che devi ripulire le risorse.

Tuttavia, puoi semplificarti la vita in questi casi. Ad esempio, potresti scimmiottare la patch $rootScopee dargli un oggetto $onRootScopeche si iscrive agli eventi emessi sul $rootScopema pulisce direttamente il gestore quando il locale$scope viene distrutto.

Il modo più pulito di applicare la patch a una scimmia $rootScopeper fornire tale $onRootScopemetodo sarebbe attraverso un decoratore (un blocco di esecuzione probabilmente lo farà anche bene ma pssst, non dirlo a nessuno)

Per assicurarci che la $onRootScopeproprietà non venga visualizzata in modo imprevisto durante l'enumerazione su $scopeusiamo Object.defineProperty()e impostiamo enumerablesu false. Tieni presente che potrebbe essere necessario uno spessore 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;
        }]);
    }]);

Con questo metodo, il codice del controller dall'alto può essere semplificato per:

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

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

Quindi, come risultato finale di tutto ciò, ti consiglio vivamente di usare $rootScope.$emit+$scope.$onRootScope .

A proposito, sto cercando di convincere la squadra angolare ad affrontare il problema all'interno del nucleo angolare. C'è una discussione in corso qui: https://github.com/angular/angular.js/issues/4574

Ecco un jsperf che mostra quanto di un impatto perfetto $broadcastporti sul tavolo in uno scenario decente con soli 100 $scopeanni.

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

risultati jsperf


Sto provando a fare la tua seconda opzione, ma visualizzo un errore: TypeError non rilevato: Impossibile ridefinire la proprietà: $ onRootScope proprio dove sto facendo Object.defineProperty ....
Scott

Forse ho rovinato qualcosa quando l'ho incollato qui. Lo uso in produzione e funziona benissimo. Dò un'occhiata domani :)
Christoph

@Scott L'ho incollato ma il codice era già corretto ed è esattamente quello che usiamo in produzione. Puoi ricontrollare che non hai un refuso sul tuo sito? Posso vedere il tuo codice da qualche parte per aiutare la risoluzione dei problemi?
Christoph

@Christoph c'è un buon modo di fare il decoratore in IE8, in quanto non supporta Object.defineProperty su oggetti non DOM?
joshschreuder,

59
Questa è stata una soluzione molto intelligente al problema, ma non è più necessaria. L'ultima versione di Angular (1.2.16), e probabilmente prima, ha risolto questo problema. Ora $ broadcast non visiterà tutti i controller discendenti per nessun motivo. Visiterà solo coloro che stanno effettivamente ascoltando l'evento. Ho aggiornato jsperf di cui sopra per dimostrare che il problema è stato risolto: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

107

Il risposta migliore qui è stata una soluzione a un problema angolare che non esiste più (almeno nelle versioni> 1.2.16 e "probabilmente precedente"), come menzionato da @zumalifeguard. Ma sono rimasto a leggere tutte queste risposte senza una soluzione reale.

Mi sembra che la risposta ora dovrebbe essere

  • utilizzare $broadcastdal$rootScope
  • ascolta usando $on dal locale$scope che deve conoscere l'evento

Quindi per pubblicare

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

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

}]);

E iscriviti

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

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

}]);

Plunkers

Se si registra il listener sul locale $scope, verrà automaticamente distrutto da $destroysolo quando il controller associato viene rimosso.


1
Sai se questo stesso modello può essere utilizzato con la controllerAssintassi? Sono stato in grado di utilizzare $rootScopel'abbonato per ascoltare l'evento, ma ero solo curioso di sapere se ci fosse un modello diverso.
edhedges,

3
@edhedges Immagino che potresti iniettare $scopeesplicitamente. John Papa scrive che gli eventi sono una "eccezione" alla sua solita regola di tenere $scope"fuori" i suoi controller (io uso citazioni perché, come menziona Controller Asancora $scope, è proprio sotto il cofano).
poshest

Per sotto il cofano intendi che puoi ancora farlo tramite l'iniezione?
edhedges,

2
@edhedges Ho aggiornato la mia risposta con controller asun'alternativa di sintassi come richiesto. Spero sia quello che volevi dire.
poshest

3
@dsdsdsdsd, servizi / fabbriche / fornitori rimarranno per sempre. Ce ne sono sempre solo uno (singleton) in un'app angolare. I controller d'altra parte, sono legati alla funzionalità: componenti / direttive / ng-controller, che possono essere ripetuti (come oggetti fatti da una classe) e vanno e vengono quando necessario. Perché vuoi che un controllo e il suo controller rimangano esistenti quando non ti servono più? Questa è la definizione stessa di una perdita di memoria.
più elegante


42

Poiché defineProperty ha un problema di compatibilità con il browser, penso che possiamo pensare di utilizzare un servizio.

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

e usalo nel controller in questo modo:

  • 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 per l'annullamento automatico dell'iscrizione quando l'ambito viene distrutto.
Federico Nafria,

6
Mi piace questa soluzione. Ho apportato 2 modifiche: (1) consentire all'utente di passare "dati" al messaggio di emissione (2) rendere facoltativo il passaggio di "ambito" in modo che possa essere utilizzato nei servizi singleton e nei controller. Puoi vedere le modifiche implementate qui: gist.github.com/turtlemonvh/10686980/…
turtlemonvh


15

In realtà l'utilizzo dell'emissione e della trasmissione è inefficace perché l'evento bolle su e giù nella gerarchia dell'ambito che può facilmente degradare in un imbottigliamento delle prestazioni per un'applicazione complessa.

Vorrei suggerire di utilizzare un servizio. Ecco come l'ho implementato di recente in uno dei miei progetti: https://gist.github.com/3384419 .

Idea di base: registrare un bus pub / evento come servizio. Quindi iniettare quel bus degli eventi ovunque sia necessario abbonarsi o pubblicare eventi / argomenti.


7
E quando un controller non è più necessario, come lo annulli automaticamente? Se non lo fai, a causa della chiusura il controller non verrà mai rimosso dalla memoria e continuerai a rilevare i messaggi. Per evitarlo dovrai rimuoverlo manualmente. L'uso di $ on non si verificherà.
Renan Tomal Fernandes,

1
questo è un punto giusto. penso che possa essere risolto da come si progetta l'applicazione. nel mio caso, ho un'app a pagina singola, quindi è un problema più gestibile. detto questo, penso che questo sarebbe molto più pulito se angolare avesse dei ganci per il ciclo di vita dei componenti in cui è possibile cablare / scartare cose come questa.
numan salati,

6
Lascio questo qui come nessuno lo ha mai detto prima. L'uso di rootScope come EventBus non è inefficiente in quanto $rootScope.$emit()bolle solo verso l'alto. Tuttavia, poiché non esiste uno scopo superiore, $rootScopenon c'è nulla di cui aver paura. Quindi, se stai solo usando $rootScope.$emit()e $rootScope.$on()avrai un sistema veloce EventBus a livello di sistema.
Christoph,

1
L'unica cosa di cui devi essere consapevole è che se si utilizza $rootScope.$on()all'interno del controller, sarà necessario ripulire il binding dell'evento, altrimenti si riassumerà mentre ne crea uno nuovo ogni volta che il controller viene istanziato e non lo fanno vieni automaticamente distrutto per te dal momento che ti impegni $rootScopedirettamente.
Christoph,

L'ultima versione di Angular (1.2.16), e probabilmente prima, ha risolto questo problema. Ora $ broadcast non visiterà tutti i controller discendenti per nessun motivo. Visiterà solo coloro che stanno effettivamente ascoltando l'evento. Ho aggiornato jsperf di cui sopra per dimostrare che il problema è stato risolto: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard

14

Utilizzando i metodi get e set all'interno di un servizio è possibile passare messaggi tra controller in modo molto semplice.

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 puoi controllare in questo modo

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

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

+1 Uno di quei metodi ovvi, una volta che ti viene detto, eccellente! Ho implementato una versione più generale con metodi set (chiave, valore) e get (chiave), un'alternativa utile alla trasmissione $.
TonyWilk,

8

Per quanto riguarda il codice originale, sembra che desideri condividere i dati tra gli ambiti. Per condividere dati o stato tra $ scope i documenti suggeriscono di utilizzare un servizio:

  • Per eseguire codice stateless o stateful condiviso tra controller: utilizzare invece i servizi angolari.
  • Per istanziare o gestire il ciclo di vita di altri componenti (ad esempio, per creare istanze di servizio).

Rif: Angular Docs link qui


5

In realtà ho iniziato a utilizzare Postal.js come bus dei messaggi tra i controller.

Ci sono molti vantaggi come un bus di messaggi come i collegamenti in stile AMQP, il modo in cui Postal può integrare w / iFrame e socket Web e molte altre cose.

Ho usato un decoratore per installare Postal su $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;
    }]);
});

Ecco un link a un post sul blog sull'argomento ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/


3

È così che lo faccio con Factory / Services e 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
I tuoi due controller non comunicano, usano solo lo stesso servizio. Non è la stessa cosa.
Greg,

@Greg puoi ottenere la stessa cosa con meno codice avendo un servizio condiviso e aggiungendo $ watch dove necessario.
Capaj,

3

Mi è piaciuto il modo in cui è $rootscope.emitstato utilizzato per ottenere l'intercomunicazione. Suggerisco la soluzione pulita ed efficace in termini di prestazioni senza inquinare lo spazio globale.

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

Per la perdita di memoria è necessario aggiungere un metodo aggiuntivo per annullare la registrazione dai listener di eventi. Comunque buon campione banale
Raffaeu

2

Ecco il modo rapido e sporco.

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

    };

}

Ecco una funzione di esempio da aggiungere in uno dei controller di pari livello:

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

    alert('Sink that ship!');                       

});

e ovviamente ecco il tuo HTML:

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

16
Perché non ti fai solo l'iniezione $rootScope?
Pieter Herroelen,

1

A partire dall'angolo 1.5 e dal focus sullo sviluppo basato sui componenti. Il modo raccomandato per i componenti di interagire è attraverso l'uso della proprietà 'request' e attraverso i binding di proprietà (input / output).

Un componente richiederebbe un altro componente (ad esempio il componente radice) e ottenere un riferimento al suo controller:

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

È quindi possibile utilizzare i metodi del componente radice nel componente figlio:

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

Questa è la funzione del controller del componente radice:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Ecco una panoramica architettonica completa: Comunicazioni componenti


0

Puoi accedere a questa funzione Hello ovunque nel modulo

Controller uno

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

secondo controller

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

Maggiori informazioni qui


7
Un po 'in ritardo alla festa ma: non farlo. Mettere una funzione nell'ambito root è simile a rendere globale una funzione, il che può causare ogni tipo di problema.
Dan Pantry,

0

Creerò un servizio e utilizzerò la notifica.

  1. Creare un metodo nel servizio di notifica
  2. Creare un metodo generico per trasmettere le notifiche nel Servizio di notifica.
  3. Dal controller di origine, chiamare notificationService.Method. Passo anche l'oggetto corrispondente a persistere se necessario.
  4. All'interno del metodo, conservo i dati nel servizio di notifica e chiamo il metodo di notifica generico.
  5. Nel controller di destinazione ascolto ($ scope.on) l'evento di trasmissione e accedo ai dati dal servizio di notifica.

Poiché in qualsiasi momento il servizio di notifica è singleton, dovrebbe essere in grado di fornire dati permanenti su tutto.

Spero che sia di aiuto


0

È possibile utilizzare il servizio integrato AngularJS $rootScopee iniettare questo servizio in entrambi i controller. È quindi possibile ascoltare gli eventi generati sull'oggetto $ rootScope.

$ rootScope fornisce due dispatcher di eventi chiamati $emit and $broadcastche sono responsabili dell'invio di eventi (possono essere eventi personalizzati) e usano la $rootScope.$onfunzione per aggiungere un listener di eventi.


0

Dovresti usare il Servizio, perché $rootscopeè l'accesso dall'intera Applicazione, e aumenta il carico, oppure puoi usare i rootparam se i tuoi dati non sono più.


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

Puoi farlo usando eventi angolari che sono $ emettono e $ trasmettono. Secondo le nostre conoscenze, questo è il modo migliore, efficiente ed efficace.

Per prima cosa chiamiamo una funzione da un controller.

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

    });
});

Puoi anche usare $ rootScope al posto di $ scope. Usa il tuo controller di conseguenza.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.