Как правильно общаться между контроллерами в AngularJS?

Как правильно общаться между контроллерами?

Я в настоящее время использую ужасную выдумку с участием 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);
    }
}

19 ответов

Решение

Редактировать: проблема, затронутая в этом ответе, была решена в angular.js версии 1.2.7. $broadcast теперь избегает пузырей над незарегистрированными областями и работает так же быстро, как $ emit.трансляционные характеристики идентичны $ emit с угловым 1.2.16

Итак, теперь вы можете:

  • использование $broadcast от $rootScope
  • слушать, используя $on из местного$scope что нужно знать о событии

Оригинальный ответ ниже

Я настоятельно советую не использовать $rootScope.$broadcast + $scope.$on скорее $rootScope.$emit+ $rootScope.$on, Первое может привести к серьезным проблемам с производительностью, как поднял @numan. Это потому, что событие будет проходить через все области видимости.

Однако последнее (используя $rootScope.$emit + $rootScope.$on) от этого не страдает и поэтому может использоваться как канал быстрой связи!

Из угловой документации $emit:

Отправляет имя события вверх по иерархии области, уведомляя зарегистрированного

Так как нет никакого объема выше $rootScope, там не происходит пузырей. Это абсолютно безопасно для использования $rootScope.$emit()/ $rootScope.$on() как EventBus.

Однако при использовании изнутри контроллеров есть одна ошибка. Если вы напрямую связаны с $rootScope.$on() в контроллере вам придется самостоятельно очищать привязку, когда $scope разрушается. Это связано с тем, что контроллеры (в отличие от сервисов) могут создаваться несколько раз за время жизни приложения, что приведет к суммированию привязок, что в конечном итоге приведет к утечкам памяти повсюду:)

Чтобы отменить регистрацию, просто послушайте $scope"s $destroy событие, а затем вызвать функцию, которая была возвращена $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);
        }
    ]);

Я бы сказал, что это не совсем конкретная вещь, так как она применима и к другим реализациям EventBus, что вы должны очистить ресурсы.

Тем не менее, вы можете сделать вашу жизнь проще для этих случаев. Например, вы могли бы обезьяна патч $rootScope и дать ему $onRootScope который подписывается на события, испускаемые на $rootScope но также непосредственно очищает обработчик, когда местный $scope разрушается.

Самый чистый способ обезьяны исправить $rootScope предоставить такие $onRootScope Метод будет через декоратор (блок выполнения, вероятно, будет делать то же самое, но pssst, не говорите никому)

Чтобы убедиться, что $onRootScope свойство не отображается неожиданно при перечислении $scope мы используем Object.defineProperty() и установить enumerable в false, Имейте в виду, что вам может понадобиться прокладка 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;
        }]);
    }]);

С помощью этого метода код контроллера сверху можно упростить до:

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

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

В качестве окончательного результата всего этого я настоятельно советую вам использовать $rootScope.$emit + $scope.$onRootScope,

Кстати, я пытаюсь убедить команду разработчиков Angular решить проблему в рамках ядра Angular. Здесь идет обсуждение: https://github.com/angular/angular.js/issues/4574

Вот jsperf, который показывает, насколько удачное влияние $broadcastприносит к столу в приличном сценарии всего с 100 $scope"S.

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

jsperf результаты

Верхний ответ здесь - это решение проблемы Angular, которая больше не существует (по крайней мере, в версиях>1.2.16 и "возможно, раньше"), как упомянул @zumalifeguard. Но я остался читать все эти ответы без фактического решения.

Мне кажется, что ответ сейчас должен быть

  • использование $broadcast от $rootScope
  • слушать, используя $on из местного $scope что нужно знать о событии

Так что опубликовать

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

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

}]);

И подписаться

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

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

}]);

Plunkers

Если вы зарегистрируете слушателя на местном $scope, он будет уничтожен автоматически $destroy сам, когда связанный контроллер удален.

Использование $rootScope.$ Broadcast и $ scope. $ On для связи с PubSub.

Также смотрите этот пост: AngularJS - Связь между контроллерами

Поскольку у defineProperty есть проблема совместимости браузера, я думаю, что мы можем подумать об использовании сервиса.

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

и использовать его в контроллере следующим образом:

  • контроллер 1

    function($scope, msgBus) {
        $scope.sendmsg = function() {
            msgBus.emitMsg('somemsg')
        }
    }
    
  • контроллер 2

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

GridLinked опубликовал решение PubSub, которое, кажется, разработано довольно хорошо. Услугу можно найти здесь.

Также схема их обслуживания:

Служба сообщений

На самом деле использование emit и broadcast неэффективно, потому что событие всплывает вверх и вниз по иерархии областей действия, что может легко перерасти в снижение производительности для сложного приложения.

Я бы предложил воспользоваться услугой. Вот как я недавно реализовал это в одном из моих проектов - https://gist.github.com/3384419.

Основная идея - зарегистрировать pubsub / event bus как сервис. Затем внедрите этот eventbus туда, где вам нужно подписаться или публиковать события / темы.

Используя методы get и set внутри службы, вы можете очень легко передавать сообщения между контроллерами.

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

в HTML HTML вы можете проверить, как это

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

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

Что касается исходного кода - кажется, вы хотите обмениваться данными между областями. Чтобы поделиться данными или состоянием между $ scope, документы предлагают использовать сервис:

  • Для запуска кода без сохранения состояния или с сохранением состояния, общего для контроллеров - используйте вместо этого угловые сервисы.
  • Для создания экземпляра или управления жизненным циклом других компонентов (например, для создания экземпляров службы).

Ссылка: угловые документы ссылка здесь

Я фактически начал использовать Postal.js в качестве шины сообщений между контроллерами.

В этом есть множество преимуществ, таких как шина сообщений, такая как привязки в стиле AMQP, способ, которым почтовые службы могут интегрировать w / iFrames и веб-сокеты, и многое другое.

Я использовал декоратор, чтобы настроить Почту на $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;
    }]);
});

Вот ссылка на пост в блоге по теме...
http://jonathancreamer.com/an-angular-event-bus-with-postal-js/

Вот как я это делаю с Factory / Services и простым внедрением зависимостей (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
]

Мне понравилось как $rootscope.emit был использован для достижения взаимосвязи. Я предлагаю чистое и эффективное решение, не загрязняя глобальное пространство.

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

Вот быстрый и грязный способ.

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

    };

}

Вот пример функции, которую нужно добавить в любой из контроллеров одного уровня:

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

    alert('Sink that ship!');                       

});

и, конечно, вот ваш HTML:

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

Начиная с версии Angular 1.5 и ее компонентной направленности разработки. Рекомендуемый способ взаимодействия компонентов - использование свойства require и привязки свойств (вход / выход).

Компоненту потребуется другой компонент (например, корневой компонент) и получить ссылку на его контроллер:

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

Затем вы можете использовать методы корневого компонента в вашем дочернем компоненте:

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

Это функция контроллера корневого компонента:

function addWatchedBook(bookName){

  booksWatched.push(bookName);

}

Вот полный обзор архитектуры: связь компонентов

Вы можете сделать это с помощью угловых событий $emit и $broadcast. Насколько нам известно, это лучший, эффективный и действенный способ.

Сначала мы вызываем функцию из одного контроллера.

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

    });
});

Вы также можете использовать $rootScope вместо $ scope. Используйте свой контроллер соответственно.

Вы должны использовать Сервис, потому что $rootscope Это доступ из всего приложения, и это увеличивает нагрузку, или вы можете использовать rootparams, если ваши данные не больше.

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

Вы можете использовать встроенный сервис AngularJS $rootScope и внедрите этот сервис в оба ваших контроллера. Затем вы можете прослушивать события, которые запускаются на объекте $rootScope.

$rootScope предоставляет два диспетчера событий с именем $emit and $broadcast которые отвечают за диспетчеризацию событий (могут быть пользовательские события) и использование $rootScope.$on функция для добавления прослушивателя событий.

Я создам сервис и буду использовать уведомления.

  1. Создать метод в службе уведомлений
  2. Создайте общий метод для широковещательного уведомления в службе уведомлений.
  3. Из контроллера источника вызовите уведомление Service.Method. Я также передаю соответствующий объект для сохранения в случае необходимости.
  4. В рамках метода я сохраняю данные в службе уведомлений и вызываю общий метод уведомления.
  5. В контроллере назначения я прослушиваю ($scope.on) широковещательное событие и получаю доступ к данным из службы уведомлений.

Поскольку в любой момент служба уведомлений является одноуровневой, она должна иметь возможность предоставлять постоянные данные.

Надеюсь это поможет

Вы можете получить доступ к этой функции привет в любом месте модуля

Контроллер один

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

второй контроллер

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

Больше информации здесь

Другие вопросы по тегам