In dieser Rubrik stellen wir Ihnen einen Artikel aus der aktuellen Ausgabe unserer Kundenzeitschrift GEDOPLAN aktuell zum Online-Lesen zur Verfügung.

Alle weiteren Ausgabe von GEDOPLAN aktuell zum Download finden Sie hier.

Clientseitige Webanwendungen mit AngularJS

Kristina Illenseer und Michael Steinhoff, agentbase AG, Paderborn

Von Google für Single-Page-Applikationen entwickelt: Das Open-Source-Framework AngularJS ist eine komplett clientseitige Lösung für die Erstellung von komplexen dynamischen Webanwendungen. Die Kommunikation zu einem Server erfolgt dabei meist über REST-Services, die in AngularJS asynchron verarbeitet werden.

„Angular is what HTML would have been, had it been designed for applications.“
(angularjs.org)

Durch Erweiterung der HTML Syntax, wiederverwendbare Komponenten, bidirektionalem Data Binding und Dependency Injection bringt AngularJS Schwung in die HTML DOM Struktur jeder Anwendung – ohne manuelles manipulatives Eingreifen.

Zu den Dingen, die der Entwickler dabei nicht mehr tun muss, gehört zum einen das Bereitstellen von Daten vom Backend für die Benutzeroberfläche und umgekehrt das Schreiben der Daten aus der Benutzeroberfläche in Backend Models. Darüber hinaus muss der Entwickler keine Low-level HTML DOM Manipulation vornehmen und das Schreiben von Boilerplate Code entfällt – Code, um den eigentlichen Code zu schreiben, z. B. bei der Registrierung von Events.

AngularJS enthält eine Reihe von Funktionalitäten und Konzepten. Darüber hinaus nutzt das Framework zur Darstellung von Inhalten sogenannte „Templates“. Ein Template in AngularJS ist die Ansicht, die sich aus HTML und AngularJS „Direktiven“ – wiederverwendbare Komponenten – und „Expressions“ – Platzhalter in der Ansicht– zusammensetzt.

Modularität

AngularJS stellt in der Haupt-Distribution eine große Bandbreite an Kernfunktionalitäten bereit. Einige Funktionalitäten wie bspw. das „Routing“ sind in externe Module ausgelagert, zudem existieren eine Vielzahl an 3rd Party Modulen u. a. für Internationalisierung, Validierung, Tabellen und Filter, Diagramme und UI-Elemente wie Spinner oder File-Upload.

Es gilt das Baukastenprinzip: Die Anwendung wird aus einzelnen Features und ggfs. anderen Frameworks (z. B. Twitter Bootstrap) zusammengesetzt, die beliebig kombinierbar sind.

Eine Anwendung besteht dabei selbst aus mindestens einem Modul, in dem die anderen Module in beliebiger Reihenfolge geladen werden:

var app = angular.module('myAppModule', ['ngRoute', 'ngI18n', 'ngStorage', 'myServicesModule']);

Zusätzlich erforderlich ist das Einbinden der zugehörigen Ressourcen für das Modul (JavaScript oder CSS Dateien) in HTML.

Ein Modul stellt einen Container für verschiedene Bereiche der Anwendung dar, so können bspw. Services in einem eigens für Services angelegten Modul gekapselt werden, das im Applikations-Modul der Anwendung als Abhängigkeit definiert wird. Die Services werden im Service Modul registriert, können durch die eingetragene Abhängigkeit aber anwendungsweit genutzt werden.

In komplexen Anwendungen wird meist unterteilt in

·         ein Modul je Feature

·         ein Modul für wiederverwendbare Komponenten wie Direktiven und Filter

·         und ein Applikations-Modul, das alle Module als Abhängigkeiten beinhaltet.

Die Struktur einer Anwendung sollte von Beginn an festgelegt werden, vor allem, wenn feststeht, dass die Anwendung auf Dauer gesehen an Funktionalitäten zunimmt. Die Ordnerstruktur sollte dabei möglichst flach gehalten werden, sodass langes Suchen über viele Unterordner hinweg vermieden werden kann.

Wiederverwendbare Komponenten

Die Erweiterung der HTML Syntax erfolgt in AngularJS durch sogenannte „Direktiven“. Sie stellen eine Art Markierung für den AngularJS HTML Compiler dar, der auf Grund der Direktive bestimmtes Verhalten zu dem HTML Element hinzufügt oder aber das Element und ggfs. seinen Inhalt auf definierte Weise im DOM hinzufügt.

AngularJS bietet im Standard eine große Menge an Direktiven, dieser umfasst z. B. Direktiven

·         für die Anwendung bzw. Komponenten der Anwendung wie ng-app und ng-controller

·         für Events bzw. User Interaktion wie ng-click oder ng-href

·         für die Anzeige von Elemente je nach Bedingungen durch ng-if oder ng-show

·         für die repetitive Darstellung eines Elements basierend auf Elementen aus einem Array mit ng-repeat

Direktiven können auf unterschiedliche Weise verwendet werden: als Element, Attribut innerhalb eines HTML Elements oder dessen CSS, vorgegeben wird dies in ihrer Definition. Die meisten standardmäßig bereitgestellten Direktiven werden als zusätzliches Attribut in einem HTML Element verwendet:

<div ng-repeat="car in cars"> {{car.model}} </div>

Analog zu Filtern bietet AngularJS die Möglichkeit eigene Direktiven zu implementieren, eine Direktive kann dabei beliebig komplex werden. Sie werden über directive() in der Anwendung registriert und müssen ein Objekt zurückgeben, auf Grund dessen der Compiler das Verhalten der Direktive ermittelt:

app.directive('helloWorld', function() {
   return {
      restrict: 'E',
      template: '<h1>Hello World!</h1>'
   };
});

<hello-world></hello-world>

Single-Page Architektur

Anwendungen, die mit AngularJS entwickelt werden, basieren i. d. R. auf einer Single-Page Architektur. Single-Page-Applikationen bestehen, wie der Name vermuten lässt, aus einer Seite deren Inhalte dynamisch geladen bzw. ausgetauscht werden. Die Backend Zugriffe einer solchen Anwendung erfolgen ausschließlich über Ajax, die verwendete Backend Technologie ist frei wählbar. Dieser Architekturstil folgt dem aktuellen Trend – hin zu Service-basierten Systemen – und bietet viele Vor- aber auch einige wenige Nachteile.

Zu einem der wichtigsten Vorteile zählt die Plattformunabhängigkeit. Durch das Verwenden von Web-Technologien für Single-Page-Applikationen, können die Anwendungen häufig bereits auf den neuesten und auch unterschiedlichsten Geräten genutzt werden.

Der dynamische Austausch von Inhalten sorgt zudem für eine benutzerfreundliche Bedienung der Anwendung. Dadurch, dass z. B. bei der Navigation zu einem neuen Inhalt keine komplett neue Seite geladen werden muss, sondern nur Teile, wird der Datenfluss über das Web stark reduziert.

Die Nutzung von komplett clientseitigen Web-Technologien bietet außerdem einen weiteren Vorteil: Neben der gewohnten Ausführung im Browser könnte die Webanwendung auch offline genutzt werden, indem die Ressourcen der Anwendung lokal auf ein Gerät kopiert werden. Dies wäre aber wohl eher ein Thema der hybriden Anwendungsentwicklung und führt an dieser Stelle zu weit.

Im Bereich der Suchmaschinenoptimierung eignen sich Single-Page-Applikationen meist weniger für klassische informationsbasierte Webseiten. Suchmaschinen können diese nur schwer durchsuchen und indizieren, da es sich stets um dieselbe Seite handelt.

Weitere Nachteile, die allerdings nicht speziell Single-Page-Applikationen betreffen, sondern allgemein für die Framework-Entwicklung gelten, sind die Menge an Browsern, die unterstützt werden sollen bzw. können. Dies schafft eine erhöhte Komplexität in der Entwicklung, aber auch einen hohen Testaufwand. Und nicht zuletzt muss der Anwender die Ausführung von JavaScript zulassen, damit die Webanwendung überhaupt ausgeführt werden kann.

MV* Entwurfsmuster

AngularJS implementiert das Entwurfsmuster Model-View-ViewModel (MVVM), das zur Trennung von Logik und Darstellung des User Interfaces dient.

Es handelt sich hierbei um eine Weiterentwicklung des Entwurfsmusters Model-View-Controller (MVC):

Das Framework unterstützt neben MVVM andere Entwurfsmuster, die auf MVC basieren, weshalb in diesem Zusammenhang auch gerne die Bezeichnung „Model-View-Whatever“ fällt.

Anstelle des Controllers steht in MVVM das „ViewModel“, durch das die Präsentationslogik von der View entkoppelt wird. Die Präsentationslogik umfasst die Bereitstellung von Funktionen und Daten zur Nutzung in der View sowie das Aktualisieren der Daten im Model. Die Bindung zwischen View und Model wird dabei durch eine deklarative Datenbindung in HTML hergestellt.

2-Wege Datenbindung

Die Bindung von Daten zwischen View und Model erfolgt in vielen Frameworks mit Templating System häufig in nur eine Richtung. Dadurch werden  Änderungen in der View nicht an das Model übertragen oder umgekehrt – außer durch eigens dafür geschriebenen Code wie Getter und Setter.

AngularJS hingegen nutzt die 2-Wege Datenbindung. Das Model ist die „single-source-of-truth“ und jede Änderung des Models wirkt sich auf die gesamte Anwendung aus. Hierfür sind keine Observer oder direkte DOM-Manipulationen notwendig, sodass die Menge an fehleranfälligem Code reduziert wird.

Die Model Änderung wirkt sich automatisch also auch in der View aus, aber wie? Realisiert wird das durch das sogenannte „Dirty Checking“. Hierbei registriert AngularJS im Hintergrund Listener für die in der View gerade verwendeten Variablen und Funktionen. Betroffen sind dabei nur die Variablen bzw. Funktionen, innerhalb des aktuellen „Scopes“ (ViewModel).

Der Überprüfungszyklus wird durch bestimmte Aktionen ausgelöst, wie etwa das Tippen in ein Eingabefeld. Voraussetzung ist die Verknüpfung des HTML Elements mit AngularJS über eine Direktive wie bspw. ng-model:

<input type="text" ng-model="name">

Die Verwendung von Variablen und Funktionen des aktuellen Scopes in einem Template erfolgt über „Expressions“. Es handelt sich hierbei um JavaScript-ähnliche Code-Schnipsel, die über „Bindings“ im Template verknüpft werden:

Hallo, {{name}}!

{{erstelleGruss()}}

Expressions sind nicht geeignet für

·         die Deklaration von Funktionen

·         Kontrollanweisungen wie Bedingungen, Schleifen und Exceptions

·         reguläre Ausdrücke.

AngularJS nutzt für die automatische Auflösung der Datenbindung Dirty Checking. Können Expressions nicht aufgelöst werden, werden sie als undefined oder null evaluiert. Bei vermehrter Nutzung von Expressions kann es dabei innerhalb einer View zu Performanceeinbußen kommen. Richtwerte liegen hier bei nicht mehr als 2000 Datenbindungen innerhalb einer einzigen View.

Eine Möglichkeit Performanceeinbußen entgegen zu wirken, sind die sogenannten „One-time Bindings“, definiert durch zwei Doppelpunkte in der Expression:

Hallo, {{::name}}!

Diese werden sobald sie einmal gesetzt sind, nicht neu kalkuliert, d. h. der zuvor im Hintergrund registrierte Listener wird wieder frei gegeben. Besonders sinnvoll sind One-time Bindings für statische Texte, die einmal aus dem Scope geladen werden müssen, aber zur Laufzeit der Anwendung nicht mehr verändert werden.

Scopes und Controller

Ein „Scope“ stellt Inhalte für das Rendern einer View bereit. Expressions, die in der View genutzt werden, liegen automatisch in einem Scope. Sie können aber auch explizit hinzugefügt werden:

$scope.name = 'Welt';

$scope.erstelleGruss = function() {
   return 'Hallo, Welt!';
};

Durch den hierarchischen Aufbau von Scopes, werden Expressions von innen nach außen evaluiert. Der „Root Scope“ bildet hierbei den ranghöchsten Scope, der in der gesamten Anwendung verwendet werden kann. Er ist vergleichbar mit einer globalen Variablen und sollte sparsam und nur für Daten und keine Funktionen genutzt werden. Je Anwendung existiert genau ein Root Scope, wobei eine Anwendung erst durch die Direktive ng-app als AngularJS Anwendung gekennzeichnet wird. Die Direktive ng-app wird i. d. R. im <html>-Tag gesetzt und nimmt als Parameter einen String für den Namen der Anwendung entgegen. Sie erzeugt automatisch einen neuen Root Scope.

„Controller“ definieren einen Scope für einen Teil des DOM, der Scope als solches kann im Controller genutzt werden. Die Deklaration eines Controllers erfolgt entweder global als JavaScript Objekt

var MainController = function() {
};

oder aber innerhalb eines AngularJS Moduls für die Anwendung.

var app = angular.module('myAppModule', []);

app.controller('MainController', function() {
});

AngularJS schlägt für die Namensgebung das Nutzen eines großen Anfangsbuchstabens und den Suffix „Controller“ vor.

Der Name des AngularJS Moduls, das als Applikations-Modul der Anwendung dient, muss dem Namen aus ng-app entsprechen:

<html ng-app="myAppModule"></html>

Damit Controller in einer View genutzt und Expressions aufgelöst werden können, müssen sie mit der View verknüpft werden. Dies geschieht entweder im Routing oder aber im HTML Template mit der Direktive ng-controller:

<div ng-controller="MainController"></div>

Ein Controller sollte stets nur die Logik für eine View enthalten. Zusätzliche Funktionalitäten oder Daten und Funktionen, die übergreifend genutzt werden, sollten in Services ausgelagert werden.

Inversion of Control

Abhängigkeiten zu eigenen oder bereits vorhandenen Komponenten werden von AngularJS per „Dependency Injection“ aufgelöst. So ist es möglich modularen und wiederverwendbaren Code zu implementieren, der besser zu warten und zu testen ist. Die Injektion erfolgt dabei über den Namen:

app.controller('MainController', ['myService', function(myService) {
}]);

Durch diese Art der Annotation werden die Namen bei Nutzung eines Minifyers bzw. Obfuscators beibehalten, sodass die Abhängigkeiten weiterhin aufgelöst werden können.

Services

AngularJS bietet eine Vielzahl an standardmäßig bereitgestellten „Services“ wie z. B. $http für das Aufrufen von URLs. Darüber hinaus werden in den meisten Anwendungen eigene Services benötigt, um bspw. Anwendungslogik in Komponenten zu kapseln, die anwendungsweit genutzt werden können. AngularJS schreibt für Services keine Namenskonvention vor.

Bei Services handelt es sich um austauschbare Objekte, die erst bei Benutzung („lazy“) instanziiert werden. Sie sind Singletons, d. h. jede Komponente, die abhängig von einem Service ist, erhält eine Referenz zu der einzig vorhandenen Instanz des Services. Genutzt werden sie mittels Dependency Injection in Controllern, Filtern, Direktiven oder anderen Services.

Unterschieden wird in die verschiedenen Typen – von einfach bis komplex:

Constant: eine unveränderbare Konstante, die z. B. zum Festlegen für wiederkehrende Strings wie Event-Namen oder URLs genutzt werden kann. Für diesen Service können keine Abhängigkeiten injiziert werden.

Value: für einfache Datentypen oder Objekte sowie Funktionen geeignet. In diesen Service können ebenfalls keine Abhängigkeiten injiziert werden. Die Werte können im Gegensatz zum Constant Service verändert werden. Es sollte jedoch nie der Value Service als solches überschrieben werden, da ansonsten die Zuweisung verloren geht.

Service: erzeugt einen neuen Service ähnlich wie der nachfolgende „Factory“ Service. Am geeignetsten sind diese Art von Services für die Erstellung von bereits vorhandenen eigenen Objekttypen, die Abhängigkeiten per Dependency Injection nutzen wollen.

Factory: erzeugt einen neuen Service mittels einer Funktion, in die Abhängigkeiten injiziert werden können. Der Rückgabewert dieser Funktion ist die Service Instanz, die per Dependency Injection in anderen Komponenten zur Verfügung gestellt wird. Der Unterschied zwischen den beiden Services „Service“ und „Factory“ liegt bei der Erstellung durch AngularJS, „Service“ wird mit new erzeugt.

Provider: stellt die Grundlage für die anderen Typen dar. Auch in diesen Service können Abhängigkeiten injiziert werden. Der „Provider“ Service implementiert eine $get-Methode und ist der einzige Service, der konfigurierbar ist. Er sollte nur genutzt werden, um einen Service bereitzustellen, der zum Anwendungsstart anwendungsweit konfiguriert werden muss.

Transformation von Daten

Einige Daten, seien es Listen, Objekte oder einfache Werte, müssen in manchen Anwendungen formatiert oder auf Grund von bestimmten Informationen verarbeitet werden. Dazu zählen u. a. Währungen oder das Selektieren von Elementen in einer Auswahlliste. AngularJS stellt hierfür die sogenannten „Filter“ bereit. Filter dienen zur Transformation von Daten und werden für Expressions durch Anhängen des „Pipe“-Operators hinzugefügt:

{{name | uppercase}}

Zudem können sie Argumente – mit Doppelpunkt eingeleitet und verkettet – entgegennehmen:

{{preis | currency:'€':0}}

AngularJS liefert standardmäßig bereits eine Auswahl an Filtern, bietet darüber hinaus aber auch die Möglichkeit eigene Filter zu implementieren. Die Verwendung der Filter unterscheidet sich hierbei nicht.

Standardmäßig vorhanden sind folgende Filter:

number formatiert eine Zahl zu einem Text und akzeptiert als Parameter die Anzahl an Dezimalstellen.

currency formiert eine Zahl zu einer Währung und akzeptiert das Währungssymbol sowie die Anzahl an Dezimalstellen als Parameter.

lowercase/uppercase formatieren einen String in Klein-/Großbuchstaben.

json konvertiert ein JavaScript Objekt in einen JSON String. Dieser Filter ist am nützlichsten für das Debuggen der Webanwendung.

date formatiert ein Date zu einem String und akzeptiert das Format und die Zeitzone als Parameter.

limitTo erzeugt ein Array / einen String mit einer limitierten Anzahl an Elementen des ursprünglichen Arrays / Strings. Als Parameter muss die Größe des neuen Arrays sowie der Start-Index angegeben werden.

orderBy sortiert ein Array nach einem Ausdruck. Akzeptiert wird außerdem ein Parameter zur Angabe, ob die Sortierung umgekehrt erfolgen soll.

filter bildet eine Untermenge eines Arrays und liefert dieses als neues Array zurück. Diesem Filter zugrunde liegt ein Ausdruck, nach dem gefiltert werden soll sowie optional ein Comparator (Funktion zum Vergleichen der einzelnen Elemente).

Die Standard Werte für AngularJS Filter sind internationalisiert, der currency-Filter bspw. erhält als Währungssymbol das Dollarzeichen.

Filter können des Weiteren auch per Dependency Injection in Controllern, Services, Direktiven und eigenen Filtern genutzt werden. Hierzu können sie auf zwei unterschiedlichen Wegen injiziert und genutzt:

1)      Injektion mit dem Suffix „Filter“

app.controller('MainController', ['filterFilter', function(filterFilter) {
   // …
   var unfilteredArray = [
      {name: 'MMuster', email: 'mmuster@mail.de' },
      {name: 'LMüller', email: 'lmueller@mail.de' },
      {name: 'HGustav', email: 'hgustav@mail.de' }
   ];

   var filteredArray = filterFilter(unfilteredArray,
                           {email: 'hgustav@mail.de'});
}]);


2)      Nutzung über den AngularJS Service $filter

app.controller('MainController', ['$filter', function($filter){
   // …
   var unfilteredArray = [
      {name: 'MMuster', email: 'mmuster@mail.de' },
      {name: 'LMüller', email: 'lmueller@mail.de' },
      {name: 'HGustav', email: 'hgustav@mail.de' }
   ];

   var filteredArray = $filter('filter')(unfilteredArray,
                           {email: 'hgustav@mail.de'});
}]);

Eigene Filter werden über filter() in einem Modul der Anwendung registriert und müssen eine Funktion zurückgeben, deren erster Parameter der zu transformierende Wert ist. Alle weiteren Parameter stellen die Argumente dar, die bei Nutzung des Filters mit angegeben werden können:

app.filter('reverseName', ['$filter', function($filter) {
   return function(name, uppercase) {
      var split = name.split(" ");
      var splitted = split[1] + ", " + split[0];
      if(uppercase) {
         splitted = $filter('uppercase')(splitted);
      }
      return splitted;
   };
}]);

Routing

Wie bereits erwähnt, führt das Konzept der Single-Page-Applikation zu einem Austausch von Inhalten innerhalb einer Seite, die jedoch, je mehr Inhalt angezeigt werden soll, größer, unübersichtlicher und schlechter zu warten wird. Diesem Zustand kann durch die Aufteilung der einzelnen Views in Templates vorgebeugt werden:

·         Anlegen eines Layout Templates, für gewöhnlich index.html genannt, mit dem Inhalt, der für alle Views in der Anwendung gilt.

·         Nutzen von partiellen Templates mit dem Inhalt für eine spezifische View. Sie werden je nach aktuellem Kontext in das Layout Template eingefügt.

URLs der Anwendung können dann auf diese Templates abgebildet werden, das sogenannte „Routing“.

Für das Realisieren von Routing in einer AngularJS Anwendung gibt es zwei gängige Module: das AngularJS Modul „ngRoute“ und das 3rd Party Modul von Angular UI „ui-router“. In diesem Artikel betrachten wir das von AngularJS angebotene Routing.

Um das Modul ngRoute nutzen zu können müssen die Ressourcen in HTML eingebunden und ngRoute als Abhängigkeit in der Anwendung eingetragen werden.

Die einzelnen „Routen“ können dann über den Service $routeProvider in der Konfigurationsphase der Anwendung erzeugt und konfiguriert werden. Der Service dient zur Verdrahtung von Template, Controller und der zugehörigen URL:

app.config(['$routeProvider', function($routeProvider) {
   $routeProvider
   . when('/', {
      templateUrl: 'views/view-produkte.html',
      controller: 'ProdukteAnsichtCtrl'
   })
   .when('/produkt-detail/:produktId', {
      templateUrl: 'views/view-produkt-detail.html',
      controller: 'ProdukteDetailCtrl',
      resolve: {
         produkt: function($routeParams, ProduktFactory) {
            return ProduktFactory.getProduktById($routeParams.produktId);
         }
      }
   })
   .otherwise({
      redirectTo: '/'
   });
}]);

Einzelne Routen werden mit when() erstellt. Beim ersten Parameter handelt es sich um eine relative URL, der zweite Parameter ist ein Objekt, das das Template (oder eine relative URL zum Template) sowie einen Controller als Eigenschaft hat. Bei Bedarf können in dem Objekt über die Eigenschaft resolve Werte definiert werden, die beim Aufruf der Route, und damit einhergehend beim Aufruf des Controllers, aufgelöst werden müssen. Diese Werte werden im Controller als Abhängigkeit über den im Routing vergebenen Namen injiziert.

Einer Route URL können Parameter hinzugefügt werden, die als solche mit einem Doppelpunkt gekennzeichnet sind. Der Zugriff auf die Parameter einer Route erfolgt über den Service $routeParams, der sowohl im Controller als auch in resolve genutzt werden kann.

Mit otherwise() wird eine URL definiert, zu der navigiert werden soll, sofern eine URL in der Anwendung nicht aufgelöst werden kann (keine entsprechende Route vorhanden).

Die Routen werden in der URL der Anwendung verwendet. Dies ermöglicht dem Benutzer das Nutzen des Browser Verlaufs und das Hinzufügen von Lesezeichen für einen expliziten Inhalt. Eine URL sieht bspw. wie folgt aus:

.../example-project/

Die Route „produkt-detail“ wird der URL nach „#/“ hinzugefügt, danach folgt der Parameter, in diesem Fall die Produkt ID.

Damit die partiellen Templates in das Layout Template dynamisch eingebettet werden können, muss im Layout Template die Direktive ng-view verwendet werden:

<div ng-view></div>

Diese Direktive dient als Platzhalter für partielle Templates und markiert den Bereich, in den sie automatisch eingebunden werden. Die Nutzung der Direktive ist allerdings eingeschränkt. Sie darf nur einmal pro Seite genutzt werden. Für komplexere Anwendungen mit vielen dynamischen Inhalten innerhalb einer Seite eignet sich das Modul ngRoute auf Grund dessen eher weniger.

Versprochen ist versprochen

Der Aufruf von Backend Services nimmt häufig viele Ressourcen in Anspruch und blockiert im schlimmsten Fall die Benutzeroberfläche. Asynchroner Code schafft Abhilfe: aber nicht einfach durch JavaScript Callbacks, sondern durch das mächtigere Feature „Promises“.

Ein Promise ist ein mögliches Ergebnis einer asynchronen Aufgabe wie bspw. der Aufruf eines REST Services. Es handelt sich um eine Art Versprechen, dass irgendwann, i. d. R. sobald ein Ergebnis der asynchronen Aufgabe vorliegt, ein Ergebnis geliefert wird.

Promises müssen immer aufgelöst werden, dies kann sowohl negativ als auch positiv geschehen. AngularJS $http-Service bspw. dient zur Kommunikation mit einem Remote HTTP Server und läuft asynchron, als Ergebnis zurückgeliefert wird ein Promise:

var promise = $http.get('http://example.com');

Das Ergebnis eines asynchronen Aufrufs ist über das zurückgelieferte Promise Objekt erhältlich. Unabhängig davon, wie ein Promise aufgelöst wird, ist es über then() zugänglich:

promise.then(successCallback, errorCallback, notifyCallback)

Als Parameter akzeptiert die Funktion „Success“, „Error“ und „Notify“ Callbacks, diese stellen wiederum jeweils eine Funktion dar, müssen allerdings nicht alle gesetzt sein. Die Callbacks können als Parameter das Ergebnis oder eine Fehlermeldung bekommen und als Ergebnis einfache Werte oder Objekte, einen Fehler oder wieder einen Promise zurückliefern.

Für promise.then(null, errorCallback) existiert die Kurzschreibweise promise.catch(errorCallback). Zudem kann, sofern das Ergebnis des Promises irrelevant ist, die Funktion promise.finally(callback, notifyCallback) genutzt werden. Sie dient zur Überwachung der Auflösung des Promises und ist vor allem für das Freigeben von Ressourcen von Nutzen.

Verwendung finden Promises z. B. in einem Controller. Es werden Daten aus einem Service geladen und währenddessen die restliche Logik ausgeführt, wie bspw. das Initialisieren von Variablen. Sobald das Ergebnis dann aus dem Service vorliegt, wird der Promise des Services erfüllt, positiv oder negativ, und die Daten können im Controller genutzt werden:

app.controller('UserDetailController', ['$scope', 'UserService', '$routeParams', function($scope, UserService, $routeParams) {
   var promise = UserService.getUserById($routeParams);
   promise.then(function(user) {
      $scope.user = user;
   }, function(error) {
      alert('Es ist ein Fehler aufgetreten: ' + error);
   });

   // restliche Logik, Variablen Deklarationen, Funktionen etc.
}]);

Aber Vorsicht: Das Ergebnis ist erst mit Erfüllung des Promises zugänglich, d. h. wir können $scope.user nicht direkt in der nächsten Zeile nutzen, dafür aber im Success Callback.

Des Weiteren können dank Promises asynchrone Aufgaben sequentiell abgearbeitet werden. Dies kann vor allem dann sinnvoll sein, wenn asynchrone Services voneinander abhängig sind.

Die Verkettung kann dabei entweder verschachtelt oder flach erfolgen.

Durch AngularJS $q-Service können eigene Funktionen asynchron implementiert werden. Dieser ist mittlerweile Teil des ECMAScript 6 Standards (ECMA 262 – Spezifikation für JavaScript Kern Features), allerdings sind noch nicht alle Funktionen verfügbar.

Tests, Tests, Tests

JavaScript ist anfällig: kaum Unterstützung vom Compiler bei Tippfehlern in Variablennamen oder bei Aufrufen nicht existenter Funktionen oder Objekte – nur ein Grund mehr zum Schreiben von Tests. Tests fügen einer Anwendung neben Stabilität auch ein gewisses Maß an Seriosität hinzu.

„We have built many features into Angular which make testing your Angular applications easy. With Angular, there is no excuse for not testing.“
(angularjs.org)

Modularer Aufbau, Dependency Injection… AngularJS implementiert Features, die das Testen von Anwendungen erleichtern. Die AngularJS Code Basis wird dabei selbst, mit bspw. mehr als 4000 Unit Tests, abgedeckt. Für das Testen von AngularJS Anwendungen existieren hilfreiche Tools wie „Karma“, „Jasmine“ und „Protractor“.

Unit Tests – Code-level Tests zum isolierten Testen einzelner Komponenten und Funktionen – können mit dem Test-Framework „Jasmine“ erstellt werden. Jasmine Tests sind strukturiert aufgebaut und nutzen „Assertions“. Die API umfasst dabei eine große Bandbreite an Funktionen u. a.:

describe() erzeugt die Test Beschreibung („Suite“). Suites können verschachtelt sein und akzeptieren einen String und eine Funktion als Parameter. Der String stellt den Namen oder einen Titel für die zu testende Funktionalität dar, die Funktion definiert die Suite. Eine Suite kann zudem mit xdescribe() explizit ausgeschlossen oder mit ddescribe() explizit einzeln ausgeführt werden.

it() enthält den Test („Spec“). Analog zur Suite werden auch hier ein String für den Namen oder Titel sowie eine Funktion zur Definition des Specs als Parameter akzeptiert. Ein Spec enthält eine oder mehrere Assertions, sogenannte „Expectations“, die über expect() angegeben werden. Genutzt werden hier vordefinierte „Matcher“ wie bspw. toBe(), toEqual() oder toMatch(), die zur Prüfung einer Übereinstimmung dienen. Darüber hinaus können eigene Matcher geschrieben und genutzt werden.

beforeEach() / afterEach()dienen zur Kapselung von wiederkehrendem Code innerhalb einer Suite, der für jeden Spec gilt. Als Parameter wird eine Funktion übergeben. Es können mehrere dieser Blöcke in einer Suite vorhanden sein.

describe('BerechnungenCtrl', function() {
   describe('$scope.summe', function() {
      beforeEach(function() {
         // …
      });
      afterEach(function() {
         // …
      });
     
it('is a number', function() {
         // …
         expect($scope.summe).toEqual(7);
      });
   });
});

Dank Depedency Injection können Abhängigkeiten relativ einfach durch Platzhalter ausgetauscht werden, den sogenannten Mock-Objekten. AngularJS stellt hierfür das Modul „ngMock“ bereit, das zur Injektion von Services sowie Mocken von Abhängigkeiten dient.

Über die Funktion module() wird ein AngularJS Modul für den Unit Test geladen, die Injektion von AngularJS Services wird durch inject() erreicht:

describe('BerechnungenCtrl', function() {
   beforeEach(module('berechnungen'));

   var
$controller;
   beforeEach(inject(function(_$controller_) {
      $controller = _$controller_;
   }));

   // …
});

Die Unterstrich-Notation von Services wie _$controller_ ist eine in der AngularJS Community weit verbreitete Konvention, um lokale Service Variablen unter demselben Namen nutzen zu können. ngMock stellt zudem neben $controller weitere AngularJS Services bereit wie bspw. $httpBackend, der zum Mocken des $http-Services dient.

Die Unit Tests können „Standalone“ durch Aufruf einer Seite ausgeführt werden. Hierzu müssen alle relevanten Ressourcen in der Seite eingebunden worden sein. Dazu gehören die Jasmine Ressourcen (JavaScript und CSS), die Anwendungsressourcen (JavaScript) und die Ressourcen, die die Unit Tests enthalten.

Alternativ können die Tests mit Hilfe von „Karma“ ausgeführt werden. Bei dem JavaScript Command-Line Tool Karma handelt es sich um einen Test-Runner für Unit Tests. Die Nutzung ist dabei in Kombination mit einem Continuous Integration Server wie Jenkins möglich.

Die Konfiguration von Karma erlaubt bspw. das Festlegen der genutzten Test-Frameworks, z.B. Browser, die Karma automatisiert für die Tests nutzen soll, oder die Art der Darstellung der Testergebnisse. Zudem werden die JavaScript Ressourcen der Anwendung in der Konfiguration angegeben. Darunter auch die benötigten Ressourcen wie AngularJS aber auch ngMock sowie die Dateien, die die Unit Tests enthalten. Die Reihenfolge ist hierbei entscheidend: Dateien, von denen andere Dateien abhängig sind, müssen zuerst genannt werden.

Karma startet einen HTTP-Server und je nach Konfiguration auch einen oder mehrere Browser. Die spezifizierten Ressourcen werden automatisch geladen, die Unit Tests ausgeführt und die Testergebnisse bspw. in der Konsole ausgegeben.

End-to-End Tests – User-level Tests zum Testen des Zusammenspiels mehrerer Komponenten – können mit Hilfe von „Protractor“ realisiert werden. Das Selenium Front-End Protractor führt Tests automatisiert in einem Browser aus, dabei interagiert das Test Framework wie ein Benutzer mit der Anwendung.

Die Konfiguration von Protractor umfasst z. B. das Festlegen des Test Frameworks (standardmäßig Jasmine) oder des für den Test zu nutzenden Browsers. Der zu nutzende Selenium Server kann ebenfalls konfiguriert werden. Zudem müssen die Dateien, die die Unit Tests beinhalten, angegeben werden.

Die Anwendung selbst muss zum Ausführen der Tests über einen Server zugänglich sein, da Protractor im Vergleich zu Karma nicht allein die Dateien nutzt, sondern die komplette Anwendung im Browser ausführt. Hierzu können Daten wie die URL zur Anwendung in der Konfiguration angegeben werden.

Protractor stellt einige globale Variablen bereit, die in den Tests genutzt werden können, darunter

·         browser – zur Navigation in der Anwendung

·         element – als Helfer Funktion zum Finden von Elementen anhand eines „Locators“ im DOM sowie die Interaktion mit dem Element über das zurückgelieferte Objekt

·         und by – „Locator“, anhand dessen Protractor weiß, wie ein bestimmtes Element gefunden werden kann, z. B. by.name, by.css, by.binding.

Aktionen wie Klick, Beeinflussen von Texten oder Lesen von Attributen werden auf dem zurückgelieferten Objekt, dem „ElementFinder“, ausgeführt. Protractor bietet viele weitere Möglichkeiten Elemente zu finden oder mit ihnen zu interagieren.

Ein bekanntes Pattern bei der Erstellung von End-to-End Tests sind die sogenannten „Page Objects“. Sie dienen dazu Elemente einer Seite und die Interaktion mit dieser an einer zentralen Stelle zu kapseln. Änderungen an der Seite in der Anwendung können so zentral an einer Stelle für alle zugehörigen Tests nachgezogen werden. Page Objects werden in der Suite mit require() eingebunden:

describe('Produkt Detail', function() {
   var produktDetailPage = require('./produkt-detail.page.js');
   // …
});

Und wen die Einfachheit für das Schreiben von Tests in AngularJS Anwendungen nicht überzeugt hat:

„Why do you need to write tests?
Because you're not Chuck Norris. Chuck Norris' code tests itself, and it always passes, with 0ms execution time.“

(andyshora.com)

Zurück in die Zukunft

AngularJS erfreut sich Google Trends zufolge zunehmend großer Beliebtheit. Neben der stetig wachsenden Community, die AngularJS aktiv nutzt oder zum Teil auch an der Entwicklung beteiligt ist, wächst auch das Framework. Dabei müssen viele Anforderungen erfüllt werden, wie Sicherheit und Stabilität und immer wieder sind neue Features gefragt.

Auf der Veranstaltung AngularConnect 2015 zeichneten Pete Bacon Darwin und Lucas Mirelmann in ihrem Vortrag die Zukunft von AngularJS auf:

·         Die Versionen 1.2.x und 1.3.x erhalten nur noch Sicherheitsupdates

·         Version 1.4.x ist die derzeit aktuelle stabile Version, die zusätzlich Bug Fixes erhalten wird

·         und Version 1.5 wird aktiv entwickelt mit neuen Features und Performanceoptimierungen.

Darüber hinaus entwickelt das AngularJS Team bereits an der Version 2.0 – aktuell in der Alpha Phase – die weitreichende Änderungen im Framework mit sich bringen wird. AngularJS 2.0 verzichtet z. B. gänzlich auf Konzepte wie Controller und Scopes und setzt stattdessen auf das Konzept von Webkomponenten. Dabei kann AngularJS 2 neben JavaScript auch mit TypeScript oder Dart verwendet werden. Im Vordergrund stehen bei der Entwicklung, abgesehen von neuen Features im AngularJS Kern, mobile Geräte, moderne Browser und nicht zuletzt weitere Performanceoptimierungen.

Die Migration von AngularJS 1.x Anwendungen auf AngularJS 2 wird durch zwei Projekte unterstützt: „ng-forward“ und „ng-upgrade“.

Das Projekt ng-upgrade beschäftigt sich mit Strategien und Ideen bzgl. der Migration von AngularJS 1 Anwendungen auf AngularJS 2. Während es eher theoretischer Natur ist stellt das Projekt ng-forward ein Modul dar, das in die Anwendung eingebunden werden kann. Unterstützt werden AngularJS 1 Versionen ab 1.3.x. Das Projekt dient dazu Entwicklern das Schreiben von AngularJS 1 Code in AngularJS 2 Syntax zu ermöglichen. Es ist laut eigener Beschreibung dabei unabhängig davon, ob Entwickler

·         überhaupt auf AngularJS 2 umsteigen werden, aber dennoch die Vorteile nutzen wollen

·         neue AngularJS 1 Projekte beginnen, für den Start aber direkt den einfachsten Migrationspfad wählen wollen

·         oder gerade aktiv auf Angular 2 migrieren und ng-forward als ersten Migrationsschritt nutzen.

Mit Unterstützung der beiden Projekte kann die Migration inkrementell erfolgen vom Lernen der neuen Syntax und Semantik über Vorbereitung der Anwendung für AngularJS 2 bis hin zum vollständigen Upgrade. AngularJS 1 und 2 sind dabei während der gesamten Migration koexistent und können vollständig miteinander gemischt werden. Vom Prinzip her sehen die Projekte die folgenden Schritte vor:

1)      Konvertieren der Syntax von AngularJS 1 Komponenten in AngularJS 2 ähnliche Syntax

2)      Upgraden dieser Komponenten auf AngularJS 2

3)      Entfernen von ng-forward

4)      Upgraden der restlichen Anwendung auf AngularJS 2

Die Schritte 1 – 2 werden solange wiederholt, bis es keine Bereiche in der Anwendung mehr gibt, die nicht ohne großen Aufwand migriert werden können. Die restlichen Teile der Anwendung werden erst im letzten Migrationsschritt berücksichtigt. Diese Art der Migration stellt nur einen möglichen Weg dar. Alternativ könnte die Migration z. B. komplett ohne ng-forward oder gar von Grund auf, ohne beide Projekte erfolgen.

Warum sollten Entwickler bei so gravierenden Änderungen eigentlich noch AngularJS 1 nutzen? Zum einen sind diese Versionen stabil und die meisten Probleme bereits behoben oder zumindest bekannt. Zum anderen setzt AngularJS 2.0 gänzlich auf ECMAScript 6 und ist damit nur für neuere Browser gedacht. Derzeit unterstützen lediglich die Browser Mozilla Firefox und Google Chrome Features aus ES6, jedoch bei weitem nicht alle. Wer ältere Browser unterstützen möchte, muss also AngularJS 1 nutzen.

Für das Erlernen von AngularJS als Neueinsteiger gehen die Meinungen weit auseinander, doch bei einer Sache sind sich alle einig: AngularJS 1 wird noch viele Jahre unterstützt werden bis AngularJS 2 vollends stabil und in der Community angekommen ist.