Wartbare Rich Web Applications mit AngularJS – Teil 1

Rich Web Applications unterscheiden sich gegenüber Thin Web Applications im höheren Grad der Funktionalität und Komplexität, die klientenseitig statt serverseitig implementiert werden. Während bei typischen Thin-Client-Architekturen das User Interface pro Seite serverseitig generiert wird, sind Rich Clients eigenständige Anwendungen, die im Browser ausgeführt werden. Damit sind benutzerfreundlichere, performantere und offlinefähige Oberflächen möglich, die im Funktionsumfang mit Desktop-Anwendungen vergleichbar sind. Diese Eigenschaften, insbesondere Offlinefähigkeit, sind speziell auch für mobil nutzbare Anwendungen bedeutend.

HTML5 hat sich hierfür mittlerweile als Alternative zu Lösungen wie Flash, Java Applets und Silverlight etabliert und bietet gegenüber proprietären Produkten einen offenen plattformunabhängigen Technologiestandard. HTML und CSS beschreiben hierbei die statische Struktur und das Design der Oberfläche, während die klientenseitige Dynamik mittels neuer HTML5-APIs und JavaScript implementiert wird.

Die Entwicklung von Rich Web Clients mittels HTML und JavaScript wird jedoch durch Wartbarkeitsprobleme erschwert, insbesondere deshalb, weil die Kombination HTML und JavaScript keine Möglichkeit vorsieht, den Client sauber zu modularisieren und zu testen. AngularJS versteht sich als HTML-Erweiterung, die sich dieser Problematik annimmt und verspricht, die Entwicklung wartbarer JavaScript-/HTML-basierter Rich Web Applications zu ermöglichen.

In dieser Artikelserie stellen wir das JavaScript-Framework AngularJS vor. Dieser erster Artikel erläutert die grundlegenden Konzepte wie die Umsetzung des Model-View-Controller-Patterns, die Erweiterung von HTML durch sogenannte Directives und das Routing. Im zweiten Artikel wird die Integration von Unit- und Ende-zu-Ende-Tests sowie die Einbindung dieser in einen Maven-Build-Prozess gezeigt.

Einführung

AngularJS wurde mit dem Ziel entworfen, HTML mit Dynamik zu versehen. Es erlaubt die Modularisierung von JavaScript-Logik sowie die Trennung statischer Oberflächenbeschreibung von dynamischer Kontrolllogik nach dem MVC-Paradigma. Die Besonderheit gegenüber Bibliotheken wie jQuery ist, dass der DOM-Baum nicht imperativ manipuliert wird. Imperative Manipulation ist ein Ansatz, der zu einer schlechten Wartbarkeit führen kann. Stattdessen wird der HTML-Code durch Attribute angereichert. Mit Hilfe dieser Attribute und JavaScript wird der HTML-Code mit Dynamik versehen.

Blog-Anwendung

Die im folgenden beschriebene Blog-Anwendung demonstriert die Implementierung einer wart- und testbaren Rich Web Application. Die Anwendung wird dabei als Single-Page-Anwendung realisiert.

Architektur-Diagramm

Als Server dient ein JBoss AS 7, der eine REST-Schnittstelle für unsere AngularJS-Anwendung bietet. Anhand der Anwendung stellen wir die wichtigsten Aspekte von AngularJS vor. Der Quellcode befindet sich auf GitHub.

AngularJS-Blog

Module

Ein Kriterium für gut wartbare Anwendungen ist eine gute Strukturierung, insbesondere bei größeren Projekten. Eine wartbare Struktur kann u.a. durch Modularisierung erreicht werden. AngularJS bietet hierzu ein eigenes Konzept an, um den JavaScript-Code zu modularisieren. Das folgende Beispiel eines solchen Moduls fasst alle Services zusammen, die im Kontext der Bloganwendung für die Anbindung des Servers zuständig sind.

[sourcecode language=”javascript”]
angular.module(‘Services’, []).
/** BlogPostService */
factory(‘BlogPostService’, [
‘$http’,
function($http) {
// …
return { // … };
}
]).

/** CommentService */
factory(‘CommentService’, [
‘$http’,
function($http) {
// …
return { // … }
]).
// …
[/sourcecode]

In Zeile 1 wird das Modul mit Hilfe der module() Funktion initialisiert. Diese erwartet die Übergabe des Modulnamens und eine Liste aller abhängigen Module. Danach werden die einzelnen Elemente des Moduls definiert. Dies erfolgt in bestimmten Blöcken, wie der im Beispiel verwendete Factory-Block. Eine Übersicht aller möglichen Blöcke und wann diese zu verwenden sind, ist hier zu finden.

Abhängigkeiten zu anderen Komponenten werden per Dependency Injection aufgelöst. Es handelt sich um ein Konzept, bei welchem die Abhängigkeiten einer Komponente zur Laufzeit konfiguriert werden, um die Testbarkeit und Wiederverwendbarkeit zu steigern. Im Beispiel wird hierzu in Zeile 4 der AngularJS-Service $http injiziert, der zur Kommunikation mit dem Server dient.

Model-View-Controller

Das Model-View-Controller-Pattern dient der Strukturierung von Software in die drei Einheiten Model, View und Controller. Dabei enthält das Model die zu präsentierenden Daten, während die View für die Präsentation dieser zuständig ist. Der Controller nimmt Benutzerinteraktionen entgegen und aktualisiert das Modell.

Zur Kommunikation zwischen Model, Controller und View verwendet AngularJS sogenannte Scopes: Namensräume, die als einfache JavaScript-Objekte abgebildet sind. Jede AngularJS-Anwendung hat genau ein Root-Scope-Objekt, von dem weitere Scope-Objekte abgeleitet werden. So verfügt jeder Controller über einen eigenen nach außen nicht sichtbaren Scope, über den die Kommunikation mit der zugeordneten View erfolgt. Nehmen wir als Beispiel den BlogPostController, der für die Erstellung der Einzelansicht eines Blog-Posts zuständig ist.

[sourcecode language=”javascript”]
// …
controller(‘BlogPostController’, [
‘$scope’,
‘BlogPostService’,
‘CommentService’,

function($scope, BlogPostService, CommentService) {
$scope.blogPost = BlogPostService.blogPost;
$scope.commentService = CommentService;
}
]).
// …
[/sourcecode]

In Zeile 9 ist zu sehen, wie dem Scope-Objekt des Controllers der CommentService als Eigenschaft hinzugefügt wird. Dieser Service enthält u.a. das Datenmodell der Kommentare zu dem Blog-Post. Da AngularJS alle Scope-Objekte überwacht, werden Veränderung eines Datenmodells sofort registriert und weitere Aktionen können ausgeführt werden. In diesem Fall würde eine Veränderung des Datenmodells der Kommentare dazu führen, dass die entsprechende View aktualisiert wird. So erscheinen neu verfasste Kommentare sofort in der Kommentarliste.

mvc_diagramm

Datenmodelle sind im AngularJS-Kontext immer Eigenschaften eines Scope-Objekts und nur in der View sichtbar, die dem Controller zugeordnet ist.
Eine Besonderheit in diesem Zusammenhang ist das bidirektionale Data-Binding zwischen View und Controller, welches über das Scope-Objekt geschieht. Die meisten Templating-Systeme ermöglichen Data-Binding nur in eine Richtung. Wenn also der Benutzer den Zustand des Datenmodells verändert, wird dies nicht registriert. Natürlich ist es möglich, Benutzerinteraktionen zu überwachen, aber in AngularJS passiert dies automatisch. Der Controller bleibt somit frei von Code zur View-Manipulation und ist damit sehr gut testbar.

Views / Partials

Views, im AngularJS-Kontext auch Partials genannt, werden in einfachem HTML definiert und durch so genannte Directives mit Dynamik (JavaScript-Logik) versehen. Dazu bringt AngularJS eine Reihe vordefinierter Directives mit, die als Attribut, Klassen- oder Element-Name deklariert werden.

Das folgende Beispiel zeigt den Einsatz der Directive ng-repeat innerhalb der View der Blog-Startseite, um auf einfachste Weise eine Liste von Blogposts anzuzeigen.

[sourcecode language=”html”]
<div id="blogPostList"
ng-controller="BlogPostListController">
<article ng-repeat="post in blogPostService.blogPosts">
<h2>{{ post.title }}</h2>
<h5>
{{ post.author.firstname }}
{{ post.author.surname }} –
{{ post.created | date:’dd.MM.yyyy H:mm’ }}
</h5>
<p>{{ post.content | blogPostPreview }}</p>
<p>
<a href="#/post/{{ post.id }}">Read more…</a>
</p>
</article>
</div>
[/sourcecode]

In Zeile 2 wird zunächst der Controller durch die Directive ng-controller zugewiesen. Somit sind nun alle Objekte und Funktionen aus dem Controller-Scope innerhalb der View sichtbar. Die Generierung der Blog-Post-Liste beginnt in Zeile 3. Durch die Zuweisung der Directive ng-repeat erfolgt für jedes Element aus dem Datenmodell (blogPosts) eine Wiederholung nach dem vorgegeben Schema. Die Ausgabe der Objektwerte findet in den geschweiften Klammern statt. Dies sind Angular-Expressions, die zur Laufzeit ausgewertet werden. Der folgende Code-Block zeigt einen Ausschnitt der entstanden Liste.

[sourcecode language=”html”]
<div id="blogPostList"
ng-controller="BlogPostListController"
class="ng-scope">
<!– ngRepeat: post in blogPostService.blogPosts –>
<article ng-repeat="post in blogPostService.blogPosts"
class="ng-scope">
<h2 class="ng-binding">
Lorem ipsum
</h2>
<h5 class="ng-binding">
Jerry Francis – 10.01.2013 10:49
</h5>
<p class="ng-binding">Lorem ipsum dolor sit amet, consectetur adipisicing elit…</p>
<p><a href="#/post/17">Read more…</a></p>
</article>

<article ng-repeat="post in blogPostService.blogPosts"
class="ng-scope">
<h2 class="ng-binding">
Sed ut perspiciatis
</h2>
<h5 class="ng-binding">
John Doe – 09.01.2013 22:12
</h5>
<p class="ng-binding">Sed ut perspiciatis, unde omnis iste natus error sit voluptatem accusantium…</p>
<p><a href="#/post/18">Read more…</a></p>
</article>
<!– … –>
</div>
[/sourcecode]

Controller

Die Kommunikation zwischen View und Controller erfolgt über den Scope eines Controllers. Alle Objekte und Funktionen, auf die in der View zugegriffen werden soll, müssen zum Scope hinzugefügt werden.

Das folgende Beispiel zeigt eine einfache Controller-Implementierung, die für die Steuerung der Blog-Post-Liste zuständig ist.

[sourcecode language=”javascript”]
// …
controller(‘BlogPostListController’, [
‘$scope’,
‘BlogPostService’,

function($scope, BlogPostService) {
$scope.blogPostService = BlogPostService;
}
]).
// …
[/sourcecode]

Ein Controller kann über den Controller-Block eines Moduls definiert werden. Die Definition erfolgt nach folgendem Muster. Zuerst wird der Controller-Name angegeben (Zeile 2). Darauf folgt eine Auflistung aller abhängigen Services und abschließend die eigentliche Implementierung des Controllers als JavaScript-Funktion (Zeile 6).

Services

AngularJS bietet die Möglichkeit, eigene Service-Objekte zu erstellen. Zudem gibt es vordefinierte AngularJS-Services, die durch das $-Zeichen zu erkennen sind. Eine Auflistung ist in der AngularJS-API-Referenz zu finden.

Als Beispiel dient der BlogPostService, der Funktionen bereitstellt, die zur Kommunikation mit der REST-API benötigt werden. Außerdem enthält er das Datenmodell der Blog-Post-Liste.

[sourcecode language=”javascript”]
// …
factory(‘BlogPostService’, [
‘$http’,

function($http) {
// …
return {
// …
/** Datenmodell: Blog-Post-Liste */
blogPosts: [],

fetchBlogPosts: function() {
var self = this;
return $http.get(restUrl).
success(function(data) {
self.blogPosts = data;
return self.blogPosts;
}).
error(function(data) {
return data;
});
},
// …
};
}
]).
// …
[/sourcecode]

Die Definition eines Service kann wie im Beispiel mit Hilfe des Factory-Blocks erfolgen. Der Aufbau gleicht dem des Controller-Blocks. Zuerst wird der Service-Name genannt. Darauf folgen die abhängigen Services und die Funktion, die das Service-Objekt zurückgibt.

Routing

Ein Problem von Single-Page-Anwendungen ist die fehlende Unterstützung des Browser-Verlaufs. D.h. typische Funktionen wie das Navigieren über die Bedienelemente des Browsers (Vor- und Zurück-Button) sowie das Setzen von Lesezeichen sind nicht ohne weiteres möglich. Um dieses Problem zu lösen, bietet AngularJS ein Routing-System. Die Definition der Routen erfolgt im Start-Modul innerhalb des Config-Blocks.

[sourcecode language=”javascript”]
// …
config([
‘$routeProvider’,

function($routeProvider) {
$routeProvider.
when(‘/’, {
templateUrl: ‘partials/blog-post-list.html’,
// …
}).
// …
when(‘/login’, {
templateUrl: ‘partials/login-form.html’
}).
// …
}
]).
// …
[/sourcecode]

Die when() Funktion des $routeProvider-Service definiert eine einzelne Route. Sie erwartet die Übergabe des Routenpfads sowie diverse optionale Parameter, wie beispielsweise templateUrl, der den Pfad zum HTML-Template der Route angibt.
Die Lösung des eigentlichen Problems, also die Unterstützung des Browser-Verlaufs, liegt darin, dass AngularJS an die URL einen Hash anhängt. Dieser wird in der URL durch das #-Symbol, auch Fragmentbezeichner genannt, kenntlich gemacht. Für die im Beispiel definierte Login-Route würde die komplette URL also folgendermaßen aussehen:

url_hash

Durch das Anhängen dieses Zusatzes erfolgt bei einem Routenaufruf ein Eintrag in den Browser-Verlauf, wodurch das Navigieren über die Bedienelemente des Browser sowie das Setzen von Lesezeichen möglich ist.

Fazit

Die Erweiterung von HTML durch Directives und das bidirektionale Data-Binding führen zu einer sauberen Trennung von statischer UI-Beschreibung und dynamischer Anwendungslogik. Der gewöhnliche Code zur DOM-Manipulation entfällt, was die Testbarkeit von Controller– und Service-Komponenten erhöht.

Komponenten mittels AngularJS-Modulen zu modularisieren erhöht Strukturierung und Wartbarkeit von JavaScript-Anwendungen. Abhängigkeiten unter den Modulen und zu AngularJS-Services werden mit Hilfe von Dependency Injection aufgelöst. Das Problem des Browser-Verlaufs von Single-Page-Anwendungen wird durch das integrierte Routing-Konzept gelöst.

Im nächsten Artikel werden wir die Integration der Unit- und Ende-zu-Ende-Tests beschreiben sowie unseren Lösungsansatz für die Einbindung der Tests in einen Maven-Build-Prozess vorstellen.

Bei Fragen und Anmerkungen kontaktieren Sie uns gerne per E-Mail:

till.hermsen [at] akquinet.de
philipp.kumar [at] akquinet.de

6 Gedanken zu „Wartbare Rich Web Applications mit AngularJS – Teil 1

  1. Hi, großes Lob, das ist so ziemlich der beste Artikel zu AngularJS, den ich bisher gefunden habe.
    Was ist denn aus den Plänen zum Teil 2 geworden? Gerade Real-Life-Themen wie die Einbindung in Maven oder Angular-Tests in einem CI System à la Jenkins bereiten vermutlich nicht nur mir Kopfzerbrechen und werden in den meisten Posts leider vernachlässigt.

      1. Hallo Torsten,

        Der Artikel hat mich sehr neugierig auf das Buch gemacht. Laut ITunes Link “läuft” das Buch aber nur mit iBooks(und die laufen nur auf Apple Produkten). Könnt ihr mir einen Bezug in einem alternativen Format ermöglichen (epub,…) ?

        Danke lg

Kommentare sind geschlossen.