AngularJS Tipps: Message-Bus für eine AngularJS-App und das Publish-Subscribe Pattern

Avatar von Norbert Schmidt

Mit diesem Artikel setze ich die Reihe AngularJS Tipps fort und beschäftige mich hier noch einmal mit der Frage, wie in einer AngularJS-App Kommunikation und Datenaustausch stattfinden kann. Allerdings liegt der Schwerpunkt in diesem Fall auf Applikationen mit konsequent modularer Struktur. Teil I der Reihe handelte von der Kommunikation zwischen AngularJS-Controllern und Teil II der Reihe vom grundlegenden Aufbau einer AngularJS-Applikation.

Was ich nun in diesem Artikel konkret vorstellen möchte, ist die Implementierung eines Message-Bus mit AngularJS, der als Notification-Service dienen soll. Das Konzept dahinter ist das Publish-Subscribe Pattern. Damit können Teile einer Applikation miteinander verknüpft werden die nicht in einer unmittelbaren Beziehung stehen sollen (Kapselung, Wiederverwendbarkeit, Testbarkeit).

Der Artikel richtet sich an Leser die bereits Erfahrungen mit AngularJS gesammelt haben und ist darum keine generelle Einführung in die Arbeit mit diesem JavaScript-Application-Framework.

Implementierung des Notification-Services

Dass es unterschiedliche Ausprägungen des Publish-Subscribe Pattern gibt und etliche Umsetzungsmöglichkeiten in JavaScript bestehen ist klar. Hier geht es aber um ein  Beispiel einer möglichen Anwendung des Patterns. So sieht dann der Message-Bus als AngularJS Notification-Service wie folgt aus:

angular.module('service.notification', [])
    .factory('notification', function ($rootScope) {
        // Event names.
        var ITEM_CREATED = 'itemCreated';
        var ITEM_DELETED = 'itemDeleted';

        // Publish the ITEM_CREATED event.
        var createItem = function (item) {
            $rootScope.$broadcast(ITEM_CREATED, {item: item});
        };

        // Subscribe to the ITEM_CREATED event.
        var onCreateItem = function($scope, handler) {
            $scope.$on(ITEM_CREATED, function(event, args) {
                handler(args.item);
            });
        };

        // Publish the ITEM_DELETED event.
        var deleteItem = function (item) {
            $rootScope.$broadcast(ITEM_DELETED, {item: item});
        };

        // Subscribe to the ITEM_DELETED event.
        var onDeleteItem = function ($scope, handler) {
            $scope.$on(ITEM_DELETED, function (event, args) {
                handler(args.item);
            });
        };

        // Reveal public API.
        return {
            createItem: createItem,
            onCreateItem: onCreateItem,
            deleteItem: deleteItem,
            onDeleteItem: onDeleteItem
        };
    });

Der Notification-Service wird hier durch eine von AngularJS bereitgestellte Factory generiert und er ist gemäß dem Module-Pattern aufgebaut. Im Prinzip besteht der Code dazu aus zwei Arten von Methoden: Publish-Methoden und Subscribe-Methoden. Eine für dieses Pattern ansonsten übliche Unsubscribe-Methode habe ich hier nicht implementiert, nur um das Beispiel einfach zu halten.

Es ist es wichtig, dass hier jeder Subscriber seinen eigenen Scope in die jeweiligen Subscriber-Methoden hineinreicht. Damit spart man sich Code, der notwendig wäre, um eine Subscriber-Liste zu managen – Code, der in anderen Versionen des Publish-Subscribe-Patterns obligatorisch ist.

Die Publish-Methoden createItem() und deleteItem() nutzen die $broadcast() Methode des $rootscope Services von AngularJS. Die Notifications inklusive Daten werden so an alle Module gesendet. Das Registrieren von Events mit den beiden Subscribe-Methoden onCreateItem() und onDeleteItem() erfolgt auf Basis der $rootScope Methode $on(). Beide Methoden bekommen den aktuellen $scope als Kontext und eine Callback-Funktion hineingereicht.

Den Notification-Service nutzen

Den neuen Service zu nutzen ist, wie man so schön sagt, straightforward. Er wird dort per Dependency Injection eingebunden, wo er benötigt wird. Und das ist im Fall meines Beispieles zunächst ein Storage-Service:

angular.module('service.storage', ['service.notification'])
    .factory('storage', function (notification) {

        ...

        var add = function (item) {
            collection.push(item);

            // Publish an application-wide ITEM_CREATED notification.
            notification.createItem(item);
        };

         var remove = function (index) {
            var item = collection[index];

            collection.splice(index, 1);

            // Publish an application-wide ITEM_DELETED notification.
            notification.deleteItem(item);
        };

        ...
    });

Die Methoden des Storage-Services werden einfach um Aufrufe der entsprechenden Notification-Methoden ergänzt. So sendet die add() Funktion das itemCreated Event und die Funktion remove() das Event itemDeleted. Und sowohl itemCreated als auch itemDeleted liefert zusätzlich den betroffenen Datensatz mit.

Als Nächstes wird der Notification-Service in zwei Modulen verwendet. Genauer, zwei Controller in voneinander unabhängigen Modulen greifen auf den Notification-Service zu:

// 1. Module "application"
angular.module('application', ['service.notification'])
    .controller('ApplicationCtrl', function ($scope, notification) {

        ...

        // Subscribe to the ITEM_CREATED event  of the storage 
        // and prepare e.g. the contents of an alert-box.
        notification.onCreateItem($scope, function (item) {
            $scope.notification.message =  '"' + item.user + '" added a new item!';
            $scope.notification.type = 'success';
            $scope.notification.active = true;
        })

        ...
    });

// 2. Module "stats"
angular.module('stats', ['service.notification'])
    .controller('StatsCtrl', function ($scope, notification) {

        ...

        // Subscribe to the ITEM_DELETED event 
        // of the storage and e.g. update some stats data.
        notification.onDeleteItem($scope, function (item) {
            $scope.updateStats(item.id, 'deleted');
        });
        ...
    });

In beiden Fällen hört hier ein Controller in einem Modul auf ein bestimmtes Event, um darauf zu reagieren. Z.B. um eine Nachricht über die erfolgreiche Speicherung eines Datensatzes anzuzeigen oder um die mitgelieferten Daten weiterzuverarbeiten.

Performance-Problem?

Da $rootScope.$broadcast() von oben nach unten die komplette Scope-Hierarchie einer AngularJS-App durchläuft, kann man sich leicht vorstellen, dass für stark verschachtelte Apps (Child-Parent-Muster) diese Vorgehensweise sehr ineffektiv ist.

Ich würde aber in so einem Fall bei $rootScope.$broadcast() bleiben und diese Methode lediglich als Kommunikationskanal zwischen Modulen einer App verwenden, die auf derselben Ebene liegen (da gibt es dann kein „Oben“ und „Unten“). Und für die Kommunikation innerhalb eines mehrschichtigen Moduls (z.B. von Controller zu Controller oder von Modul zu Sub-Modul) würde ich einen einfachen, nicht event-basierten Service vorschlagen.

In diversen Fragen/Antworten auf Stackoverflow zu diesem Thema findet man auch die Idee für eine Implementierung des Publish-Subscribe Patterns die auf der $emit() Methode des $rootScope Services beruht. Auch das ist interessant und es ist sicherlich wert, ausprobiert zu werden.

Source-Code und Demo-Applikation

Die Code-Beispiele für diesen Artikel sind einer selbstgeschriebenen Demo-Applikation, „The Shoutbox“, entnommen. Der komplette Source-Code dazu liegt auf GitHub unter: https://github.com/nosch/the-shoutbox. Außerdem gibt es noch eine Live-Demo unter http://shoutbox.schmidt-netzwerk.de:

the-shout-box

Lesetipps zum Thema

Alle Artikel der Reihe „AngularJS-Tipps“

  1. Controller-Kommunikation, Datenaustausch per Routing, AJAX-Error-Handling
  2. Modularer Aufbau einer AngularJS-Applikation
  3. Message-Bus für eine AngularJS-App und das Publish-Subscribe Pattern

 

Avatar von Norbert Schmidt

Kommentare

Eine Antwort zu „AngularJS Tipps: Message-Bus für eine AngularJS-App und das Publish-Subscribe Pattern“

  1. Lesenswert: AngularJS Tipps: Message-Bus für eine AngularJS-App und das Publish-Subscribe Pattern http://t.co/hdUE9Ku3Ye

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert


Für das Handling unseres Newsletters nutzen wir den Dienst HubSpot. Mehr Informationen, insbesondere auch zu Deinem Widerrufsrecht, kannst Du jederzeit unserer Datenschutzerklärung entnehmen.