Dieser Artikel ist der erste Teil der Reihe „AngularJS Tipps“. Er beschäftigt sich mit drei Problemstellungen zu denen Best-Practise-Tipps gegeben werden sollen: (1) Kommunikation zwischen Controllern via Services, (2) Datenaustausch zwischen Controllern per Routing und AJAX-Errors an einer zentralen Stelle einer Web-App abfangen. Alle diese Themen drehen sich um die Frage, wie sich eher unabhängige Teile einer AngularJS-App miteinander verdrahten lassen, wie sich also ein Autausch von Daten und Informationen herstellen lässt.
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.
1. Controller und Services
Wie kommunizieren Controller im Rahmen einer AngularJS-Applikation miteinander? Natürlich können sich Controller mithilfe der Scope-Chain Daten teilen. Hierzu müssen die Controller nur ineinander verschachtelt sein. Dadurch ergibt sich aber eine starre Organisation der Applikation, die schnell zu Problemen führen kann, wenn das Projekt wächst und damit unübersichtlicher wird.
Wesentlich eleganter ist es stattdessen Services zu implementieren, über die Controller Daten erhalten und austauschen. Natürlich ist das Kapseln von Logik in Services schon längst kein Geheimtip mehr. Hier geht es aber konkret um die Möglichkeiten die AngularJS dazu bietet.
Ein einfacher, aber voll funtionsfähiger Service kann in AngularJS folgendermaßen aussehen:
angular.module('service.storage', []) .factory('storage', function () { var collection = []; var get = function () { return collection; }; var add = function (item) { collection.push(item); }; var remove = function (index) { collection.splice(index, 1); }; // Reveal public API. return { getItems: get, addItem: add, removeItem: remove }; });
Dieser Storage-Service, der Einträge aus einem Array-Storage liest, löscht oder neue hinzufügt, wird nun per Dependency Injection in diejenigen Controller hineingereicht, die Zugriff auf den Storage benötigen:
angular.module('items', ['service.storage']) .controller('ItemListCtrl', function ($scope, storage) { $scope.items = storage.getAll(); ... }) .controller('ItemFormCtrl', function ($scope, storage) { $scope.save = function (data) { storage.addMessage(data); }; .. });
So müssen weder in jedem Controller Methoden für den Datenzugriff existieren, noch muss ein ausgewählter Controller diese Aufgaben übernehmen und per starrer Vererbung an andere Controller weitergeben.
2. Routing und Datenaustausch
Es gibt einige Fälle in denen Controller-Logik abhängig von der aktiven Route ablaufen soll. Umgesetzt werden kann das mit Hilfe des resolve Attributs des $routeProviders. Mit ihm können via Routing Daten in einen Controller injiziert werden.
Der $routeProvider ist ein wichtiger und häufig verwendeter Service den das AngularJS-Modul ngRoute zur Verfügung stellt und mit dem die Routen einer AngularJS-Applikation konfiguriert werden.
Hier ist ein typisches Beispiel für eine solche Routen-Konfiguration:
angular.module('taskCenter', ['ngRoute']) // Inject $routeProvider, a ngRoute service. .config('$routeProvider', function ($routeProvider) { $routeProvider // Configure the routes of the application. .when('/', { templateUrl: 'view/home.tpl.html' }) .when('/tasks', { templateUrl: 'view/task-list.tpl.html', controller: 'TaskListCtrl', }) // Define a default route. .otherwise({ redirectTo: '/' }); });
Das Modul ngRoute wird per Dependency Injection hineingereicht. Damit kann dann $routeProvider als Service genutzt werden. Die Syntax ist recht einfach und intuitiv. Der Methode when() wird ein Pfad als Zeichenkette und ein Konfigurationsobjekt für die Route übergeben. Mit dem Config-Objekt werden unter anderem das Template, resp. der Pfad zum Template und der zuständige Controller festgelegt. Mit der Methode otherwise() definiert man eine Default-Route.
Der $routeProvider erlaubt es aber auch Daten abhängig von einer Route in einen Controller hineinzureichen. Hierzu dient das resolve Attribut des Config-Objekts:
... .when('/task/:id?', { templateUrl: 'view/task-form.tpl.html', controller: 'TaskFormCtrl', resolve: { // Define a "task" dependency. task: function (TaskResource, $route) { if ($route.current.params.id) { // Get and return an existing task object by ID. return TaskResource.getById($route.current.params.id); } else { // Return an empty task object. return new TaskResource(); } }] } }); ...
Im obigen Beispiel geht es um eine Route für den Aufruf eines Formulars mit dem neue „Tasks“ angelegt oder bestehende bearbeitet werden. Dem resolve Attribut ist hier ein Objekt zugewiesen worden in dem die Eigenschaft „task“ als anonyme Funktion definiert wurde. Ein Daten-Service namens TaskResource und der von AngularJS breitgestellte Service $route werden an diese Funktion übergeben. Je nachdem, ob der URL-Parameter „id“ gesetzt ist, wird „task“ ein bereits existierendes oder ein neues, leeres Task-Objekt zugewiesen.
Übrigens, wenn es sich beim Task-Objekt um ein Promise-Objekt handeln sollte, dann wird die Route erst aufgelöst, wenn auch das Promise erfüllt wird.
Das Attribut „task“ kann nun wie jede andere Dependency in einen Controller injiziert und dort verarbeitet werden:
... // Inject "task" into the controller. .controller('TaskFormCtrl', function ($scope, task) { $scope.task = task; $scope.save = function () { // Use the task object to save changes. $scope.task.$save(); }; ...
Natürlich kann die Logik, die das resolve Attribut beinhaltet, wesentlich komplizierter werden. In so einem Fall würde man die Logik zumindest in eine Funktion auslagern. Am besten aber übernimmt ein Service diese Aufgabe, der dann in der gesamten Applikation genutzt werden kann.
3. AJAX-Response-Errors abfangen
Kommunikation innerhalb einer Web-App schließt auch die Informationen über aufgetretene Fehler mit ein. Und ein besonderes Problem stellen dabei Errors von asynchron ablaufenden Requests dar. AngularJS stellt in der Form eines HTTP-Interceptors einen Service bereit, mit dem Response Errors von AJAX-Requests an zentraler Stelle abgefangen werden können.
Der $http Service ist ein Core-Modul von AngularJS. Es stellt eine API für AJAX- Requests bereit und schließt ebenfalls die Möglichkeit mit ein, eigene Interceptors für request, requestError, response und responseError zu registrieren. Üblicherweise ist ein solcher Interceptor als Service-Factory definiert:
angular.module('service.httpInterceptor', []) .factory('httpInterceptor', function ($q, $location) { return { // Decide what to do in case of HTTP response errors. responseError: function (rejection) { switch (rejection.status) { case 401: // Unauthorized $location.path('/login'); break; case 500: // Internal Server Error console.error(rejection); break; } return $q.reject(rejection); } }; });
Die Service-Definition folgt dem Module-Pattern und liefert im Kern ein Objekt mit Methoden zurück. In diesem Beispiel wird die responseError Methode überschrieben. Je nach HTTP Status Code wird dann auf den Error reagiert.
Die Registrierung des Interceptors erfolgt in einer config() Methode eines Modules, also während ein Modul geladen wird:
angular.module('application.config', ['service.httpInterceptor']) .config([ '$httpProvider', // Register your interceptor service. function ($httpProvider) { $httpProvider.interceptors.push('httpInterceptor'); } ]) ...
Auch hier sollte klar sein, dass komplexe Interceptor-Logik am besten in eigene Services ausgelagert werden sollte.
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:
Lesetipps zum Thema
- Services: http://docs.angularjs.org/guide/services
- Scopes in AngularJS: https://github.com/angular/angular.js/wiki/Understanding-Scopes
- Routing: http://docs.angularjs.org/api/ngRoute.$routeProvider
- HTTP-Interceptors: http://docs.angularjs.org/api/ng/service/$http#interceptors
Schreibe einen Kommentar