JavaScript-Framework AngularJS: HTML aufgepimpt
Auf den ersten Blick kommt AngularJS wie ein Baukasten für ungeduldige Feierabend-Entwickler daher. Die optisch schlicht gehaltene Startseite der Homepage [1] vermittelt grob die vermeintliche Grundidee des Frameworks: die Erweiterung von HTML, um Benutzereingaben und Komponenten interagieren zu lassen.
Doch keiner dieser Eindrücke wird dem Framework tatsächlich gerecht. Denn das Projekt [2] entpuppt sich als ein komplexes Zusammenspiel moderner Technologien und birgt höchste Flexibilität für professionelle Frontend-Entwicklung. Dieser Artikel beleuchtet einige grundlegende Konzepte anhand des auf GitHub bereitgestellten Beispiels „OpenWeather App““ [3]. Eine Anwendung zur ortsbezogenen Suche nach Wetterdaten, die diese nach Tagen ordnet und in Panels darstellt.
HTML als Basis für den Bau
Die zunehmend browserübergreifende Unterstützung von DOM-Standards macht HTML, JavaScript und CSS heute zu Schlüsseltechnologien für den Einsatz im Frontend-Bereich dynamisch wachsender Applikationen. Viele JavaScript-Frameworks wählen dafür diverse Ausprägungen des Model-View-Controller-Konzepts (MVC) und verzichten auf die Möglichkeit, HTML um eigene Namensräume zu erweitern. Letztere erlauben individuelle Elemente und Attribute innerhalb einer spezifischen „HTML-Domäne“.
An diesem Punkt setzt AngularJS an und erklärt die Verwendung von HTML zu seinem zentralen Konzept: Tags, Direktiven und Attribute mit dem Präfix „ng-“ erweitern das HTML-Vokabular und bilden eine flexible Möglichkeit, die Interface-Beschreibung zu präzisieren. Der direkte Eingriff in das DOM, wie ihn etwa jQuery pflegt, soll nach Möglichkeit komplett vermieden werden.
Auf diese Weise lassen sich etwa Kontrollstrukturen definieren (ng-repeat), Ereignisbehandlungen sowie für den Kontext benötigte Controller-Objekte referenzieren oder die in einem zentralen Routing-Objekt definierte Haupt-View (ng-view) einbinden. In unserem App-Beispiel wird das Hauptmodul „openWeatherApp“ im Wurzel-Knoten (<html>) der Index-Datei mit Hilfe von ng-app referenziert und somit implizit beim ersten Aufruf der Seite geladen.
Modularisiertes JavaScript
JavaScript-Applikationslogik wird bei AngularJS in Module gekapselt. Grund dafür ist eine weitere zentrale Eigenschaft des Frameworks: Dependency Injection (DI). Vereinfacht ausgedrückt erlaubt DI den Objekten, Abhängigkeiten nur zu benennen. Um die eigentliche Lokalisierung und Erzeugung der benötigten Objekte beziehungsweise Ressourcen kümmert sich das Framework. Ein Objekt braucht damit nur so viel von seiner Umgebung zu wissen, wie für die Erfüllung seiner eigentlichen Aufgabe nötig ist.
Dies macht den Code wesentlich übersichtlicher und vereinfacht den Austausch einzelner Komponenten. Außerdem lassen sich Programm-Module dank DI effektiver testen. Denn durch das Abtreten der Verantwortung für abhängige Objekte kann die eigentlich zu testende Funktionalität besser isoliert werden. Während des Tests werden Abhängigkeiten entsprechend auch nur in Form so genannter „Mock-Objekte“ bereitgestellt.
angular.module('openWeatherApp', [ 'ngRoute', 'openWeatherApp.filters', 'openWeatherApp.services', 'openWeatherApp.directives', 'openWeatherApp.controllers', 'iso-3166-country-codes' ]) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/forecast', { templateUrl: 'partials/forecast.html', controller: 'OpenWeatherCtrl' }); $routeProvider.otherwise({ redirectTo: '/forecast' }); }]);
Listing 1
Im konkreten OpenWeatherApp-Beispiel definiert das Hauptmodul der app.js weitere Module wie Controller, Services und Filter; AngularJS lädt diese wie erwähnt allein durch diese Definition automatisch nach. Der injizierte Router wird so konfiguriert, dass über den Aufruf der Route „/forecast“ das gleichnamige View-Template eingebunden, also ins DOM gerendert, und mit dem Controller openWeatherCtrl assoziiert wird.
AngularJS und der MVC-Ansatz: So sieht er aus
Der Controller agiert nun als Schaltzentrale der UI-Logik gemeinsam mit der Laufzeitumgebung, die Veränderungen in bestimmten DOM-Objekten überwacht. Dabei sorgt das Framework für eine klare Trennung der Darstellung vom Verhalten der UI. Auch AngularJS folgt also in Grundzügen dem MVC-Ansatz:
- Das Modell ist eine Sammlung einfacher JavaScript-Objekte und kann somit aus beliebig primitiven oder komplexen Objekten aufgebaut werden. Anders als etwa in Ember.js gibt es zur Erzeugung von Modellen kein Objekt, von dem abgeleitet wird.
- Die View wird deklarativ in HTML geschrieben und bindet Modelle, Controller und Zugriffsmethoden an bestimmte Elemente, definiert aber kein Verhalten.
- Der Controller ist für das Verhalten verantwortlich und stellt für die Applikations- bzw. Geschäftslogik notwendige Daten, Eigenschaften und Methoden bereit. Er besitzt keine Kenntnisse über die View.
Der entscheidende Kniff ist, dass der Controller seine öffentlichen Eigenschaften und Zugriffsmethoden über die reservierte Variable $scope zur Verfügung stellt. Somit kann das Framework Datenbindungen zu Elementen der View herstellen, welche den spezifischen Controller verwenden.
Zurück zum App-Beispiel: In der View kann entweder über vordefinierte Ortsnamen-Buttons oder frei über ein Textfeld nach Wetterdaten für einen bestimmten Ort gesucht werden. In der View forecast.html wird dieses Formular beschrieben, mitsamt seiner Bindungen an das Modell bzw. den $scope des Controllers.
<form class="form-inline" role="form"> <span class="btn-group" > <button ng-repeat="item in exampleLocations" ng-click="setLocation(item)" type="submit" class="btn btn-default">{{item}} </button> </span> <div class="form-group"> <label class="sr-only" for="location">City</label> <input ng-model="location" type="text" class="form-control" id="location" placeholder="City"> </input> </div> <button ng-click="getForecastByLocation(location)" type="submit" class="btn btn-primary">Search!</button> </form>
Listing 2
Zwei Wege, eine Wahrheit
Vergleichbar mit Ember.js oder Meteor wird auch von AngularJS automatisch ein Two-Way-Data-Binding für Variablen erzeugt, die in der View referenziert werden. Das bedeutet, dass sich Änderungen im Datenmodell unmittelbar in der View widerspiegeln – und umgekehrt. Basis dafür ist wieder der $scope als gemeinsamer Sichtbarkeitsbereich im Kontext des jeweiligen DOM-Elements.
In diesem Zusammenhang wird auch gerne von der „single source of truth“ gesprochen, welche Modell und View synchron und den Programmierer bei Laune hält. Denn das Konzept der bidirektionalen Datenbindung entlastet die oft mühselige und fehleranfällige Programmierung von View-Aktualisierungen durch direkte DOM-Manipulation. Letzteres ist bei AngularJS ohnehin nicht gern gesehen.
angular.module('openWeatherApp.controllers', []) // Controller for "open weather map" api data search .controller('OpenWeatherCtrl', ['$scope','openWeatherMap','exampleLocations','ISO3166', function($scope,openWeatherMap,exampleLocations,ISO3166) { // Expose example locations to $scope $scope.exampleLocations = exampleLocations; // On initialization load data for first example entry $scope.forecast = openWeatherMap.queryForecastDaily({ location: exampleLocations[0] }); // Get forecast data for location as given in $scope.location $scope.getForecastByLocation = function() { $scope.forecast = openWeatherMap.queryForecastDaily({ location: $scope.location }); }; // Set $scope.location and execute search on API $scope.setLocation = function(loc) { $scope.location = loc; $scope.getForecastByLocation(); }; } ] )
Listing 3
Der Controller der Wetter-App (openWeatherCtrl) fällt letztlich knapper aus, als man zunächst denken mag. So wird etwa die Nutzung der Wetterdaten-API in ein Service-Modul verlagert und als Abhängigkeit injiziert. Die Aufgabe des Controllers besteht im Wesentlichen in der Bereitstellung der Suchfunktion (getForecastByLocation) sowie der dabei zurückgelieferten Wetterdaten. Diese werden wiederum direkt in eine $scope-Variable geschrieben, sodass die View automatisch an den Stellen aktualisiert wird, wo Daten aus diesem Modell referenziert werden.
Ausgaben verarbeiten
Hierbei kommen schließlich „Expressions“ zum Einsatz, erkennbar an der Einfassung in doppelten geschweiften Klammern. Diese Marker erlauben die Ein- und Ausgabeverarbeitung, also etwa die Validierung oder Formatierung von Werten, des definierten $scope. Im Beispiel der Wetter-App werden aus dem Objekt mit den Vorhersagedaten für einen bestimmten Tag Temperaturwerte mittels des Filters „number“ auf eine Nachkommastelle formatiert ausgegeben.
AngularJS stellt dafür eine Reihe nützlicher Filter für unterschiedliche Zwecke bereit. Um eigene Filter zu definieren, stellt das Framework darüber hinaus ein Konstrukt („filter“) zur Verfügung. Auch für eigene HTML-Direktiven liefert Google ein passendes Werkzeug: Mit „directives“ lassen sich flexible Controller-Funktionen mit View-Eigenschaften verknüpfen und in eigene HTML-Elemente gießen. Für die Wetter-App etwa Panels mit datumsbezogenen Wetterdaten und zugehörigem Bild. Weitere Beispiele finden sich in der umfangreichen Dokumentation des Frameworks.
Tests inklusive
Gerade bei der Entwicklung großer oder kontinuierlich wachsender Projekte spart das automatisierte Testen wertvolle Zeit zur formalen Validierung der implementierten Funktionalität. Zudem ermutigt es, von Anfang an klar strukturierten Code in kleinen überschaubaren Funktions-Einheiten zu verfassen.
AngularJS erleichtert das Schreiben und Testen des Codes darüber hinaus noch mittels klarer Trennung des Controller-Codes von der View. Dependency Injection und auch die Vermeidung von direkter DOM-Manipulation spielen für die Testbarkeit ebenfalls eine große Rolle. Explizite Abhängigkeiten werden minimiert und Code-Blöcke können besser isoliert getestet werden. Aber das Testen einer AngularJS-Applikation wird nicht nur ermöglicht, sondern durch die Bereitstellung zweier grundlegender Setups geradezu schmackhaft gemacht:
- Skeletons und Skripte für das Unit-Testing mit dem Testing-Framework Jasmine und dem Test-Runner karma
- eine zusätzliche Runner-API für das „End-to-End Testing“, also Testen des UI-Verhaltens bei Benutzerinteraktion
Als Beispiel soll ein End-to-End-Test aus der Wetter-App dienen. Die Interface-Tests werden hierbei in so genannten „scenarios“ definiert und mit den vom JS-Testing-Framework Jasmine bereitgestellten Suite-Funktionen describe/it/expect beschrieben, spezifiziert und auf erwartete Rückgabewerte geprüft. Dabei bedient sich der Test-Runner tatsächlicher Browser-Umgebungen, es werden also Instanzen des aktuellen Browsers gestartet, um das Verhalten unter den normalen Bedingungen einer laufenden Rendering-Engine zu testen. Beim Beschreiben der Tests können Elemente über Selektoren direkt angesprochen und das Benutzerverhalten simuliert werden. Somit lässt sich etwa der Klick auf ein bestimmtes Element und dessen erwartete Veränderung an anderer Stelle hervorrufen und testen.
describe('OpenWeather App', function() { beforeEach(function() { browser().navigateTo('../../app/index.html'); }); describe('Forecast view', function() { beforeEach(function() { browser().navigateTo('#/forecast'); }); it('should render forecast when user navigates to /forecast', function() { expect(element('[ng-view] form button[type="submit"]').text()).toMatch(/Search!/); }); it('should map the value of an "instant city forecast" button to the input field', function() { element('[ng-view] form .btn-group > button:first-child').click(); expect(element('[ng-view] form input#location').attr('value')).toBe('Hamburg'); }); }); });
Listing 4
Fazit
Unter der „HTML-Haube“ von AngularJS steckt ein anspruchsvolles Framework mit dem Ziel, einer Reihe von modernen Entwicklungsparadigmen gerecht zu werden: MVC bzw. MVVM, modularer Aufbau mit Dependency Injection und ein ausgefeiltes Test-System. Gerade bei komplexen Entwicklungsvorhaben und in Team-Situationen mit einer klaren Rollenaufteilung zwischen UI/UX-Designern und JavaScript-Entwicklern sollte die Evaluation von AngularJS in Betracht gezogen werden.