AngularJS in der Praxis: Neues Frontend für radio.de
Beim Relaunch der internationalen Plattform radio.net sowie dem dazugehörigen Radio-Aggregator radio.de stand die Überarbeitung des gesamten Stacks an – von Backend und Suche, API und Web-Frontend bis hin zu den mobilen Apps für iOS, Android, Windows Phone und Blackberry.
Modulares Design = modulare Entwicklung
Um bei einem Projekt dieser Größenordnung eine parallele Entwicklung zu gewährleisten, ist es notwendig, Arbeitspakete und technische Komponenten genau zu analysieren und voneinander zu entkoppeln. Ziel der agilen Entwicklung des Projekts waren deshalb lauffähige Zwischenstände und die Möglichkeit, die geplanten Interaktionsmöglichkeiten jederzeit prüfen zu können.
Unser Team verantwortete die Umsetzung des gesamten Frontends. Dafür gab es drei Hauptanforderungen von Seiten der Auftraggeber:
- Aufbau einzelner Seiten mittels JavaServer Pages (JSP)
- Keine Unterbrechung des radio.de-Players beim Seitenwechsel
- Umsetzung eines modularen, responsiven Designs
Die zentrale Idee der neuen Informationsarchitektur von radio.de ist die Strukturierung in Modulen, aus denen sich die einzelnen Seiten zusammensetzen. Wichtig war es vor allem, dem Kunden gegenüber diese neue Modularisierung von Beginn an zu verdeutlichen. Bei der Erstellung von Wireframes und Layout-Mockups verstehen Kunden die Ergebnisse nämlich oft als in sich abgeschlossene Seiten und weniger als ein System aus Komponenten, dessen einzelne, funktionale Bestandteile auf mehreren Seiten Verwendung finden.
Warum AngularJS?
AngularJS setzt wie auch andere JavaScript-Frameworks auf das Konzept DRY (Don’t Repeat Yourself). Es gab uns mit seinem modularisierten Ansatz, der performanten Zwei-Wege-Datenbindung, die im View wie im Model auf Zustandsänderungen reagieren kann, sowie mit den Testmöglichkeiten die nötigen Mittel an die Hand, um die Anforderungen der komplexen UI-Elemente umzusetzen.
Im Vergleich zu Ember.js fallen die Vorgaben für die Strukturierung von Dateien weniger strikt aus. So werden zwar Dateien anhand ihrer Namen im Code erkannt, für die Ordnerstruktur lässt AngularJS aber kompletten Freiraum für eigene Anpassungen.
Single-Page Web-Apps, Caching und SEO
Eine Vorgabe von radio.de war die Nutzung von JSP für die Ausgabe der Seitentemplates. Da das gesamte Serversystem von radio.de und radio.net in Java implementiert ist, bildeten neben der API die JSP-Templates unsere Schnittstelle zum Backend. Hierdurch konnten wir für die Entwicklung einen Hybrid-Ansatz nutzen. Die auf dem Server generierten Templates dienen der schnellen Rückgabe der initialen Daten. Hier geschehen bereits alle strukturellen Arbeitsschritte: zum einen die für SEO relevante Ausgabe der HTML-Strukturen, zum anderen die Auslagerung des für AngularJS sonst extra anfallenden Pre-Renderings auf Server-Seite.
Der AngularJS-Web-Client setzt auf diese strukturelle Basis auf und fokussiert sich auf die dynamische Anzeige und das Austauschen von Inhalten, ohne Seiten neu laden zu müssen. Durch die MVC-Logik von AngularJS konnten wir so von Beginn an das Verhalten der User-Interface-Komponenten implementieren, auch ohne eine funktionale API.
Die grundlegenden Informationen über die Struktur der API-Callbacks lagen bereits vor und waren definiert. Wir erstellten anhand dieser unsere eigenen Mockdaten, implementierten als Erstes alle Mechanismen zur Anzeige und Verarbeitung von Daten und entkoppelten uns für die interne Entwicklung von Abhängigkeiten zu anderen Dienstleistern.
Die interne Toolchain
Zur Code-Versionierung nutzten wir Git, als zentrale Verwaltung die als Open Source kostenfreie GitLab-Community-Edition.
Für die Organisation der Entwicklungs-Branches setzte unser Team auf die Vorgaben der Git-Erweiterung „Git Flow“. Einzelne Arbeitspakete wurden jeweils in einzelnen Feature-Branches angelegt und bearbeitet.
Für die reine Frontend-Entwicklung der Features des AngularJS-Clients kam der Static-Site-Generator Middleman zum Einsatz. Damit liefen auf jedem Entwicklerrechner dieselben Tools und jeder Entwickler konnte unabhängig von den anderen seine Arbeitspakete implementieren und lokal prüfen.
Werkzeuge und Arbeitsabläufe
Für jedes Projekt ist eine strukturierte Herangehensweise mit Rücksicht auf die Arbeitsschritte, die eigene Arbeitsorganisation sowie die jeweiligen Projektziele essenziell. Ein reines „Abfeuern” von Tools und Technologien kann nicht funktionieren, wenn sich dies nicht im Projektkontext rechtfertigen lässt und es keinen Mehrwert für alle Beteiligten erzielt.
Für uns funktionierten die bisherigen Tools bei der Nutzung von AngularJS besonders gut. Gleichzeitig erwachte nach der Einarbeitung aller Entwickler auch die Kreativität: Wir passten die Code-Struktur der AngularJS-App unseren Bedürfnissen an, um die Arbeit für alle weiter zu verbessern und die grundlegenden Arbeitsprozesse sowie den Wissenszuwachs in der AngularJS-Entwicklung im Team tiefer zu verwurzeln.
Qualitätskontrolle leicht gemacht
Ein Feature galt als abgeschlossen, wenn ein Kollege jeweils die Komponente des anderen zumindest im Ansatz mit ihm durchgesprochen hat. Hierbei fielen vor allem zu Beginn Unterschiede im Coding-Stil auf. Über den Projektverlauf glich sich dieser durch den regelmäßigen Austausch an. Zudem etablierte sich ein gemeinsames Verständnis von der Gesamtstruktur, etwa wie einzelne Komponenten und Services aufeinander aufbauen und miteinander interagieren, was eine gemeinsame Wissensbasis geschaffen hat.
Die herausgearbeiteten Coding-Standards paarten wir mit klassischen Methoden der Code-Analyse. So nutzten wir Grunt für unseren Build-Prozess zum Kompilieren und Komprimieren aller Sources. Grunt führt darüber hinaus auch eine statische Code-Analyse – das Linting – für den gesamten JavaScript-Code und das SCSS durch, um Fehler im Code aufzuspüren. Damit diese Tasks nicht repetitiv von Hand ausgeführt werden mussten, setzten wir Git-Commit-Hooks, welche die Tasks vor jedem Einchecken ausführten. Bei Fehlern bricht der Checkin mit aussagekräftigen Fehlermeldungen ab. Dank dieses Vorgehens befindet sich ausschließlich syntaktisch korrekter Code in unserem Repository.
Nach Abschluss eines Features wurde der feature-Branch jeweils im develop-Branch zusammengeführt („merged“). Diesen nutzen wir als unseren Integrationsbranch. Hier ließen sich auf einem Preview-Server das Zusammenspiel der Komponenten anschauen und das Zwischenergebnis überprüfen. Der master-Branch wurde von uns für fertige Releases genutzt, die für Deployments beim Kunden entsprechend markiert („tagged“) wurden.
Die Verzeichnisstruktur der Applikation
AngularJS folgt nur lose dem Paradigma von „Convention over Configuration“. Wir entschieden uns dazu, komponentenbasiert zu denken, und übertrugen den modularen Aufbau der Seite entsprechend auf die Ordnerstruktur (inspiriert von Cliff Meyers Gegenüberstellung von verschiedenen Organisations-Ansätzen).
Alle Komponenten/Module bekamen eigene Ordner, in denen jeglicher für die Logik verantwortliche Code abgelegt wurde. Die jeweiligen Views lagerten wir gesondert im Ordner „Views” aus. Modulübergreifende Logik legten wir im „common“-Verzeichnis ab. Hier befinden sich zum Beispiel nützliche Tools und Bibliotheken für Angular selbst wie etwa das Tracking-Modul „angulartics“ oder JavaScript-Bibliotheken wie jQuery oder lodash.
Verzeichnisstruktur der radio.de-App
. |—— radioApp.js |—— common | |—— controllers.js | |—— directives.js | |—— services.js |—— components | |—— instantSearch | | |—— instantSearchController.js | | |—— instantSearchDirective.js | | |—— instantSearchService.js | |—— tracklisting | |—— tracklistingController.js | |—— tracklistingDirective.js |—— views |—— tracklisting.html
Listing 1
RequireJS und zentralisierte Entkopplung
Für eine effiziente modulare Umsetzung sind zwei Paradigmen der Software-Entwicklung besonders wichtig: Dependency Injection und die „Asynchronous Module Definition” (AMD). AngularJS nutzt die Dependency Injection, um Abhängigkeiten zwischen einzelnen logischen Komponenten zur Laufzeit korrekt aufzulösen. Dennoch kann dieses korrekte Registrieren von Komponenten und der Überblick über verfügbare Funktionalitäten ab einem gewissen Umfang auch in Angular sehr unübersichtlich werden.
Zur generellen Strukturierung unseres Codes beziehungsweise unserer Dateien fiel unsere Wahl auf die bereits in unseren vorherigen Großprojekten erfolgreich genutzte AMD. Neben der Wiederverwendbarkeit einzelner Codefragmente erlaubt AMD, nur jeweils benötigte Module zu laden.
RequireJS ist eine weit verbreitete Implementierung der AMD-API, die es ermöglicht, unsere Module synchron sowie asynchron zu laden. Der von RequireJS bereitgestellte Optimizer erzeugt im letzten Schritt in der Entwicklung jeweils die minifizierte und optimierte Produktiv-Version des JavaScript-Codes.
AMD in der Praxis
Wir definierten zunächst Basis-Module für Controller, Direktiven und Services. Diese wurden als Abhängigkeit in der radioApp.js dem eigentlichen Hauptmodul (radioApp) zugeführt.
radioApp.js
(function (define) { "use strict"; define(function (require) { var angular = require('angular'); ... // required Directives require('common/directives'); ... // required Controllers require('common/controllers'); ... require('components/tracklisting/tracklistingController'); ... // required Services require('common/services'); ... var dependencies = [ 'controllers', 'services', 'directives', ... ]; var radioApp = angular.module('radioApp', dependencies); ... return radioApp; }); }(define));
Listing 2
Das Beispiel zeigt, wie sich an Basis-Modulen zum Beispiel Controller anhängen lassen, so dass nicht alle Definitionen auf Ebene der radioApp.js deklariert werden müssen. So gelang auch der modulare Aufbau des Projekts.
controllers.js
(function (define) { "use strict"; define(['angular'], function(angular) { return angular.module('controllers', []); }); }(define));
Listing 3
tracklistingController.js
(function (define) { "use strict"; define([ 'common/controllers', 'components/tracklisting/tracklistingDirective', ... ], function (controllers) { controllers.controller('tracklistingController', [...], function(...) { ... }]); }); }(define));
Listing 4
Was bedeutet das genau? Die Funktion erstellt ein AngularJS-Modul mit RequireJS, welches selbst keine Funktionen implementiert, sondern stattdessen als eine Art Verzeichnis für alle verfügbaren Module und Funktionalitäten fungiert. Hier werden also Untermodule aufgelistet, die dann dem Hauptmodul als Abhängigkeit „zugeführt“ werden. Diese Untermodule können unter anderem Angular-Erweiterungen wie beispielsweise das Tracking-Modul „angulartics” sein oder, wie zuvor beschrieben, JavaScript-Bibliotheken wie jQuery oder lodash. Wir definierten in diesem Modul somit ausschließlich, welche anderen Komponenten die App kennen und nutzen soll, und ließen die anderen Module wissen, welche Möglichkeiten ihnen zur Verfügung stehen.
Dadurch ergab sich ein globales Modul für Controller. Dieses „Hauptcontroller-Modul” übernimmt das Laden aller definierten Controller. Genauso handhabten wir übrigens auch Services und Direktiven. Diese Zentralisierung ermöglichte uns zwei Dinge: Die Entwicklung im Rahmen der Modularität ließ sich von allen besser nachverfolgen. Entwickler hatten die Möglichkeit, neue Module zentral hinzufügen. Und die Blicke der Kollegen wurden bei jedem Checkout auf diese „Hauptdatei” gelenkt, so dass sie auf Neuerungen und Erweiterungen sofort reagieren und die neu zur Verfügung stehenden Möglichkeiten und Libraries nutzen konnten.
Fazit
Die durch radio.de vorgegebene modulare Denkweise half unseren Entwicklern dabei, bestehende Arbeitsweisen und Erfahrungen besser auf die Projekt-Anforderungen anzuwenden. Einen stetigen Arbeitsablauf gewährleistete die vor allem bei Commits „konfliktfreie“ Zusammenarbeit. Die selbst erarbeitete Code-Struktur erleichterte neuen Teammitgliedern den Einstieg ins Projekt. Und der Hybrid-Ansatz mit JSP und AngularJS bot den großen Vorteil, uns voll auf die Stärken des Frameworks konzentrieren zu können.
Mit der allgemein als steil beschriebenen Lernkurve von AngularJS machten aber auch wir Bekanntschaft: Konventionen selbst zu erarbeiten und die von anderen Sprachen abgeleiteten Entwurfsmuster effizient einzusetzen war für uns in dieser Größenordnung neu. Im Nachhinein betrachtet sind derlei Mittel aber notwendig, um komplexe JavaScript-Projekte im Team abzubilden.
JavaScript-Frameworks sind nicht einfach nur Sammlungen nützlicher Funktionen und Coding-Weltanschauungen, sondern verbessern bei richtigem Einsatz die Arbeitsorganisation und die Qualität der Ergebnisse. Ohne die Unterstützung von AngularJS wäre ein Projekt dieser Komplexität und Größe kaum zu stemmen gewesen. Intern konnten wir dadurch im Gesamtprojekt sehr agil vorgehen, einen für die interne Zusammenarbeit effizienten und effektiven Weg herausarbeiten und als Ergebnis ein performantes Produkt liefern.
Heutzutage würde ich statt AMD unbedingt auf den neuen ES6 Modul Standard setzen und mit Babel kompilieren.
Ersteinmal guter Artikel. Muss aber sagen, das das SEO Argument für HTML-Nachladen nicht so korrekt ist. Zwar führt der Google Crawler intern JS aus, aber HTML und Text snippets die nachgeladen werden, sind meistens nicht indexiert. Das ist das größte Problem von Single-Page-Apps.
Aber sonst Top.
Alles schön und gut, aber ich empfinde das als mega lästig den Radiosender erst hören zu können wenn ich zu der jeweiligen subdomain wechsel. Der gewohnte User klickt auf ein Logo einer Website um zur Startseite zu kommen, dabei hörts dann auf weiterzuspielen