Jaki jest prawidłowy sposób komunikacji między kontrolerami w AngularJS?

473

Jaki jest właściwy sposób komunikacji między kontrolerami?

Obecnie używam okropnej krówki obejmującej 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);
    }
}
wyblakłe
źródło
36
Całkowicie dyskusyjne, ale w Angular zawsze powinieneś używać $ window zamiast natywnego obiektu okna JS. W ten sposób możesz go usunąć w swoich testach :)
Dan M
1
Proszę zobaczyć komentarz w odpowiedzi poniżej ode mnie dotyczący tego problemu. $ broadcast nie jest już droższy niż $ emit. Zobacz link do jsperf, do którego się tam odwoływałem.
zumalifeguard

Odpowiedzi:

457

Edycja : Problem rozwiązany w tej odpowiedzi został rozwiązany w wersji angular.js w wersji 1.2.7 . $broadcastteraz unika bulgotania się nad niezarejestrowanymi zakresami i działa tak szybko, jak $ emit. Wydajności emisji są takie same jak emisji z kątową 1.2.16

Teraz możesz:

  • użyj $broadcastz$rootScope
  • słuchaj za pomocą $on lokalnego,$scope który musi wiedzieć o wydarzeniu

Oryginalna odpowiedź poniżej

Radzę nie używać $rootScope.$broadcast+, $scope.$onale raczej $rootScope.$emit+ $rootScope.$on. To pierwsze może powodować poważne problemy z wydajnością, o czym wspomniał @numan. Dzieje się tak, ponieważ wydarzenie rozbije się na wszystkie zakresy.

Jednak to ostatnie (użycie $rootScope.$emit+ $rootScope.$on) nie cierpi z tego powodu i dlatego może być używane jako szybki kanał komunikacji!

Z dokumentacji kątowej $emit:

Wysyła nazwę zdarzenia w górę poprzez hierarchię zasięgu, powiadamiając zarejestrowanego

Ponieważ nie ma powyższego zakresu $rootScope, nie ma miejsca na bulgotanie. Używanie $rootScope.$emit()/ $rootScope.$on()jako EventBus jest całkowicie bezpieczne .

Jest jednak jedna gotcha, gdy używasz go z poziomu kontrolerów. Jeśli łączysz się bezpośrednio $rootScope.$on()z kontrolerem, musisz samodzielnie usunąć powiązanie, gdy lokalny $scopezostanie zniszczony. Wynika to z faktu, że kontrolery (w przeciwieństwie do usług) mogą być wielokrotnie tworzone przez cały czas istnienia aplikacji, co powoduje zsumowanie powiązań, co ostatecznie spowoduje przeciek pamięci w całym miejscu :)

Aby wyrejestrować, tylko słuchać na swojej $scope„s $destroyzdarzenia, a następnie wywołać funkcję, który został zwrócony przez $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);
        }
    ]);

Powiedziałbym, że tak naprawdę nie jest to kwestia specyficzna pod kątem, ponieważ dotyczy to również innych implementacji EventBus, że musisz oczyścić zasoby.

W takich przypadkach możesz jednak ułatwić sobie życie. Na przykład, możesz małpować łatkę $rootScopei dać jej $onRootScopesubskrypcję zdarzeń emitowanych na, $rootScopeale także bezpośrednio czyści program obsługi, gdy lokalny $scopezostanie zniszczony.

Najczystszym sposobem na załatanie małpy w $rootScopecelu zapewnienia takiej $onRootScopemetody byłoby zastosowanie dekoratora (blok uruchamiania prawdopodobnie zrobi to dobrze, ale pssst, nie mów nikomu)

Aby upewnić się, że $onRootScopewłaściwość nie pojawi się nieoczekiwanie podczas wyliczania ponad $scope, używamy Object.defineProperty()i ustawiamy enumerablena false. Pamiętaj, że możesz potrzebować podkładki 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;
        }]);
    }]);

Dzięki tej metodzie kod kontrolera z góry można uprościć w celu:

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

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

Dlatego jako ostateczny wynik tego wszystkiego radzę użyć $rootScope.$emit+ $scope.$onRootScope.

Przy okazji, staram się przekonać zespół kątowy do rozwiązania problemu w rdzeniu kątowym. Trwa dyskusja tutaj: https://github.com/angular/angular.js/issues/4574

Oto jsperf, który pokazuje, jak duży wpływ $broadcastna stół przynosi porządny scenariusz w zaledwie 100 $scope.

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

wyniki jsperf

Christoph
źródło
Próbuję wykonać drugą opcję, ale pojawia się błąd: Uncaught TypeError: Nie można przedefiniować właściwości: $ onRootScope w miejscu, w którym wykonuję Object.defineProperty ....
Scott
Może coś spieprzyłem, wklejając to tutaj. Używam go w produkcji i działa świetnie. Zajrzę jutro :)
Christoph
@ Scott Wkleiłem go, ale kod był już poprawny i jest dokładnie tym, czego używamy w produkcji. Czy możesz dwukrotnie sprawdzić, czy nie masz literówki na swojej stronie? Czy mogę gdzieś zobaczyć Twój kod, który pomoże rozwiązać problemy?
Christoph
@Christoph jest dobrym sposobem na wykonanie dekoratora w IE8, ponieważ nie obsługuje Object.defineProperty na obiektach innych niż DOM?
joshschreuder
59
To było bardzo sprytne rozwiązanie problemu, ale nie jest już potrzebne. W najnowszej wersji Angulara (1.2.16) i prawdopodobnie wcześniej problem ten został rozwiązany. Teraz program $ broadcast nie odwiedzi każdego kontrolera potomka bez powodu. Odwiedzą tylko tych, którzy faktycznie słuchają tego wydarzenia. Zaktualizowałem jsperf, o którym mowa powyżej, aby wykazać, że problem został już rozwiązany: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard
107

Najważniejszą odpowiedzią było obejście problemu Angular, który już nie istnieje (przynajmniej w wersjach> 1.2.16 i „prawdopodobnie wcześniej”), jak wspomniał @zumalifeguard. Ale nadal czytam wszystkie te odpowiedzi bez faktycznego rozwiązania.

Wydaje mi się, że odpowiedź powinna być teraz

  • użyj $broadcastz$rootScope
  • słuchaj za pomocą $on lokalnego,$scope który musi wiedzieć o wydarzeniu

Więc opublikować

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

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

}]);

I subskrybuj

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

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

}]);

Plunkers

Jeśli zarejestrujesz słuchacza na poziomie lokalnym $scope, to zostanie zniszczony automatycznie przez $destroysiebie , gdy wiąże się kontroler zostanie usunięty.

najszerszy
źródło
1
Czy wiesz, czy tego samego wzorca można użyć ze controllerAsskładnią? Byłem w stanie użyć $rootScopesubskrybenta do nasłuchiwania tego wydarzenia, ale byłem tylko ciekawy, czy istnieje inny wzorzec.
edhedges
3
@edhedges Wydaje mi się, że można to zrobić $scopebezpośrednio. John Papa pisze, że wydarzenia są jednym „wyjątkiem” od jego zwykłej zasady trzymania $scope„z dala” od swoich kontrolerów (używam cytatów, ponieważ jak wspomina, Controller Asnadal ma $scopeto pod maską).
najdalej
Czy pod maską masz na myśli, że nadal możesz się do niej dostać przez wstrzyknięcie?
edhedges
2
@edhedges Zaktualizowałem swoją odpowiedź controller asalternatywną składnią zgodnie z żądaniem. Mam nadzieję, że o to ci chodziło.
najwyżej
3
@dsdsdsdsd, usługi / fabryki / dostawcy pozostaną na zawsze. Zawsze jest jeden i tylko jeden z nich (singletony) w aplikacji Angular. Z drugiej strony kontrolery są powiązane z funkcjonalnością: komponenty / dyrektywy / kontroler ng, które można powtarzać (jak obiekty wykonane z klasy) i przychodzą i odchodzą w razie potrzeby. Dlaczego chcesz, aby kontrolka i jej kontroler istniały, gdy już ich nie potrzebujesz? To jest właśnie definicja wycieku pamięci.
najdalej
42

Ponieważ definProperty ma problem ze zgodnością przeglądarki, myślę, że możemy pomyśleć o korzystaniu z usługi.

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

i użyj go w kontrolerze w następujący sposób:

  • kontroler 1

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

    function($scope, msgBus) {
        msgBus.onMsg('somemsg', $scope, function() {
            // your logic
        });
    }
Singo
źródło
7
+1 za automatyczne anulowanie subskrypcji, gdy zakres zostanie zniszczony.
Federico Nafria
6
Podoba mi się to rozwiązanie. 2 zmiany, które wprowadziłem: (1) zezwalają użytkownikowi na przekazywanie „danych” do komunikatu emisji (2) sprawiają, że przekazywanie „zakresu” jest opcjonalne, aby można go było stosować w usługach singleton, a także w kontrolerach. Możesz zobaczyć zmiany wprowadzone tutaj: gist.github.com/turtlemonvh/10686980/...
turtlemonvh
20

GridLinked opublikował rozwiązanie PubSub , które wydaje się być całkiem dobrze zaprojektowane. Serwis można znaleźć tutaj .

Również schemat ich obsługi:

Usługa przesyłania wiadomości

Ryan Schumacher
źródło
15

W rzeczywistości użycie emisji i emisji jest nieefektywne, ponieważ zdarzenie przesuwa się w górę iw dół hierarchii zasięgu, co może łatwo zmienić się w butelkę wydajności dla złożonej aplikacji.

Sugerowałbym skorzystać z usługi. Oto jak niedawno wdrożyłem go w jednym z moich projektów - https://gist.github.com/3384419 .

Podstawowy pomysł - zarejestruj autobus pubowy / eventowy jako usługę. Następnie wstrzyknij ten eventbus tam, gdzie trzeba subskrybować lub publikować wydarzenia / tematy.

numan salati
źródło
7
A jeśli kontroler nie jest już potrzebny, jak go automatycznie anulować? Jeśli tego nie zrobisz, ze względu na zamknięcie kontroler nigdy nie zostanie usunięty z pamięci i nadal będziesz otrzymywać wiadomości. Aby tego uniknąć, musisz usunąć, a następnie ręcznie. Użycie $ na tym nie nastąpi.
Renan Tomal Fernandes
1
to słuszna kwestia. myślę, że można to rozwiązać przez sposób tworzenia aplikacji. w moim przypadku mam aplikację na jednej stronie, więc jest to łatwiejszy do rozwiązania problem. Powiedziawszy to, myślę, że byłoby to znacznie czystsze, gdyby kątowe miały haki cyklu życia komponentu, w których można było drutować / rozłączać takie rzeczy.
numan salati
6
Po prostu zostawiam to tutaj, ponieważ nikt wcześniej tego nie powiedział. Użycie rootScope jako EventBus nie jest nieefektywne, ponieważ $rootScope.$emit()tylko pęcherzyki w górę. Ponieważ jednak nie ma zakresu powyżej, $rootScopenie ma się czego bać. Więc jeśli tylko używasz $rootScope.$emit()i $rootScope.$on()będziesz miał szybki systemowy EventBus.
Christoph
1
Jedyną rzeczą, o której musisz wiedzieć, jest to, że jeśli używasz $rootScope.$on()wewnątrz kontrolera, musisz wyczyścić powiązanie zdarzenia, ponieważ w przeciwnym razie będą one sumowane, ponieważ tworzy nowe za każdym razem, gdy kontroler jest tworzony i nie zostać automatycznie zniszczony, ponieważ wiążesz się $rootScopebezpośrednio.
Christoph
W najnowszej wersji Angulara (1.2.16) i prawdopodobnie wcześniej problem ten został rozwiązany. Teraz program $ broadcast nie odwiedzi każdego kontrolera potomka bez powodu. Odwiedzą tylko tych, którzy faktycznie słuchają tego wydarzenia. Zaktualizowałem jsperf, o którym mowa powyżej, aby wykazać, że problem został już rozwiązany: jsperf.com/rootscope-emit-vs-rootscope-broadcast/27
zumalifeguard
14

Korzystając z metod get i set w ramach usługi, można bardzo łatwo przekazywać wiadomości między kontrolerami.

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

w HTML HTML możesz to sprawdzić w ten sposób

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

<div ng-controller='SecondController'>
    {{rslt}}
</div>
Załaduj ponownie
źródło
+1 Jedna z tych oczywistych metod, gdy raz się o nich dowiesz - doskonale! Zaimplementowałem bardziej ogólną wersję z metodami set (klucz, wartość) i get (klucz) - użyteczną alternatywą dla $ broadcast.
TonyWilk,
8

Jeśli chodzi o oryginalny kod - wygląda na to, że chcesz udostępnić dane między zakresami. Aby udostępnić Dane lub Stan pomiędzy $ scope, dokumenty sugerują skorzystanie z usługi:

  • Aby uruchomić kod bezstanowy lub stanowy współużytkowany przez kontrolery - Zamiast tego użyj usług kątowych.
  • Aby utworzyć instancję lub zarządzać cyklem życia innych komponentów (na przykład, aby utworzyć instancje usługi).

Ref: Angular Docs link tutaj

pkbyron
źródło
5

Właściwie zacząłem używać Postal.js jako magistrali komunikatów między kontrolerami.

Ma wiele zalet, takich jak szyna komunikatów, takich jak powiązania w stylu AMQP, sposób, w jaki poczta może zintegrować w / iFrame i gniazda sieciowe, i wiele innych rzeczy.

Użyłem dekoratora, żeby skonfigurować pocztę na $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;
    }]);
});

Oto link do postu na blogu na ten temat ...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/

jcreamer898
źródło
3

Tak to robię z Fabryką / Usługami i prostym wstrzykiwaniem zależności (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
]
Oto Brglez
źródło
1
Twoi dwa kontrolery nie komunikują się, używają tylko tej samej usługi. To nie to samo.
Greg
@Greg możesz osiągnąć to samo przy mniejszym kodzie, dzieląc usługę i dodając $ zegarki w razie potrzeby.
Capaj
3

Podobał mi się sposób $rootscope.emitwykorzystania komunikacji wewnętrznej. Sugeruję czyste i wydajne rozwiązanie bez zanieczyszczania globalnej przestrzeni.

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');
Shikhar Chauhan
źródło
W przypadku wycieku pamięci należy dodać dodatkową metodę wyrejestrowania się z detektorów zdarzeń. W każdym razie dobra trywialna próbka
Raffaeu
2

Oto szybki i brudny sposób.

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

    };

}

Oto przykładowa funkcja do dodania w dowolnym kontrolerze rodzeństwa:

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

    alert('Sink that ship!');                       

});

i oczywiście jest twój HTML:

<button ngclick="sendorders()">Sink Enemy Ships</button>
Peter Drinnan
źródło
16
Dlaczego po prostu nie wstrzykujesz $rootScope?
Pieter Herroelen
1

Począwszy od Angular 1.5 i skupia się na rozwoju opartym na komponentach. Zalecany sposób interakcji między komponentami polega na użyciu właściwości „need” i powiązaniach właściwości (wejścia / wyjścia).

Komponent wymagałby innego komponentu (na przykład komponentu głównego) i uzyskałby odwołanie do kontrolera:

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

Następnie możesz użyć metod komponentu głównego w komponencie potomnym:

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

To jest funkcja kontrolera głównego komponentu:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Oto pełny przegląd architektury: Komunikacja komponentowa

Kevinius
źródło
0

Możesz uzyskać dostęp do tej funkcji hello w dowolnym miejscu modułu

Kontroler jeden

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

drugi kontroler

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

Więcej informacji tutaj

Prashobh
źródło
7
Trochę późno na imprezę, ale: nie rób tego. Umieszczenie funkcji w zakresie głównym jest podobne do uczynienia funkcji globalnej, co może powodować różnego rodzaju problemy.
Dan Pantry
0

Stworzę usługę i użyję powiadomienia.

  1. Utwórz metodę w usłudze powiadomień
  2. Utwórz ogólną metodę rozgłaszania powiadomień w usłudze powiadomień.
  3. Z kontrolera źródła wywołaj powiadomienie ServiceService.Method. Podaję również odpowiedni obiekt, aby zachować w razie potrzeby.
  4. W ramach tej metody utrwalam dane w usłudze powiadomień i wywołuję rodzajową metodę powiadamiania.
  5. W kontrolerze docelowym słucham ($ scope.on) zdarzenia rozgłoszeniowego i uzyskuję dostęp do danych z usługi powiadomień.

Ponieważ w dowolnym momencie usługa powiadomień jest pojedyncza, powinna być w stanie zapewnić utrwalone dane w całym systemie.

Mam nadzieję że to pomoże

Rahul
źródło
0

Możesz skorzystać z wbudowanej usługi AngularJS $rootScopei wstrzyknąć tę usługę do obu kontrolerów. Następnie można nasłuchiwać zdarzeń uruchamianych na obiekcie $ rootScope.

$ rootScope udostępnia dwa wywoływacze zdarzeń, $emit and $broadcastktóre są odpowiedzialne za wywoływanie zdarzeń (mogą być zdarzeniami niestandardowymi) i używają $rootScope.$onfunkcji do dodawania detektora zdarzeń.

Shivang Gupta
źródło
0

Powinieneś korzystać z Usługi, ponieważ $rootscopejest to dostęp z całej aplikacji, a to zwiększa obciążenie lub używasz rootparamów, jeśli twoich danych już nie ma.

abhaygarg12493
źródło
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);
}
Amin Rahimi
źródło
0

Możesz to zrobić, używając zdarzeń kątowych, które to $ emit i $ broadcast. Zgodnie z naszą wiedzą jest to najlepszy, wydajny i skuteczny sposób.

Najpierw wywołujemy funkcję z jednego kontrolera.

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

    });
});

Możesz także użyć $ rootScope zamiast $ scope. Użyj odpowiednio kontrolera.

Peeyush Kumar
źródło