One Page to rule them all

Moderne JavaScript-Applikationen – ein Überblick
Kommentare

Single-Page-App? Nein, damit ist nicht die App einer Partnerbörse gemeint. So mancher denkt bei dem Begriff vielleicht sogar an eine „Landing Page“ zur Vorstellung eines neuen Produkts. Gemeint sind damit jedoch Webapplikationen basierend auf der Sprache JavaScript, welche vorwiegend darauf abzielen, das Benutzererlebnis zu verbessern.

Das Internet wurde nicht geschaffen für „Web-Apps“. Es wurde ursprünglich entwickelt, um Informationen mit Relationen zu anderen Informationen – per Hyperlinks – zur Verfügung zu stellen. Mit der raschen und stetigen Entwicklung der Technologien rund um das Internet ergeben sich allerdings immer mehr Möglichkeiten, Softwareprodukte für das Web zu entwickeln. Internetfähige Geräte sind aus dem modernen Haushalt und den Hosentaschen nicht mehr wegzudenken, und die Erwartungen an performante Anwendungen mit intuitiven Benutzeroberflächen höher denn je. Erfüllt eine Software diese Anforderungen nicht, wird sie für viele Benutzer schnell uninteressant.

Smartphones sind heute so leistungsfähig, dass es möglich ist, komplexe Webapplikationen zu entwickeln, die sich in Funktionalität und Benutzerfeeling von nativen Apps kaum unterscheiden – nur eben im Browser ausgeführt werden. Und mit der HTML5-Spezifikation steht Entwicklern mittlerweile ein Pool verschiedener Technologien zur Verfügung, der dieselben Funktionen wie nativ installierte Software im Web ermöglicht.

Single-Page-Apps (SPA) – oft auch Rich Internet Applications genannt – sind Webapplikationen, die hauptsächlich auf der Sprache JavaScript sowie weiteren Technologien wie HTML und CSS basieren und starken Fokus auf das Benutzererlebnis legen. Der Name bezieht sich auf das Laden eines einzelnen HTML-Dokuments bei der ersten Anfrage eines Clients. Dieses Dokument enthält Anweisungen zum Download weiterer Dateien wie Style Sheets und den JavaScript-Code, oft sogar das gesamte benötigte Markup. Sobald der Browser diese Dateien geladen hat, befindet sich die funktionsfähige Software beim Client. Weitere Kommunikation mit einem Server wird nun via AJAX und oft JSON anstatt XML als Datenformat realisiert. Auch die Anbindung von WebSockets o. Ä. ist denkbar. Der Server dient häufig nur noch als API zum Lesen und Manipulieren von Daten, wodurch die Clients viel flexibler sind, was die Benutzeroberfläche und das Verwalten von Zuständen angeht. Aufgrund der Zustandslosigkeit des HTTP-Protokolls ist dieser Ansatz mit serverseitigen Technologien allein jedoch nur schwer zu erreichen, denn es besteht keine dauerhafte Verbindung zwischen Browser und Server: auf einen Request erfolgt ein Response. JavaScript-Objekte allerdings befinden sich im Speicher des Clients und können somit jederzeit verwendet und mit einem Backend synchronisiert werden, ohne dass ein neues Laden der Seite erforderlich ist.

Warum Single-Page-Apps?

Wann und warum sollte man den Einsatz einer SPA in Erwägung ziehen? Ein sehr schönes Anwendungsbeispiel für eine Single-Page-App ist u. a. die neue SoundCloud-Website. SoundCloud hat in einem Blogeintrag vom Juni 2012 erwähnt, dass ein wichtiges Feature für sie das so genannte „Continuous Playback“ sein wird [1]: Benutzer können ein Lied starten und anschließend weitere Tätigkeiten durchführen, ohne dass die Musik anhält. Das erfordert das Navigieren durch Seiten, ohne gleich die ganze Seite vollständig neu laden zu müssen, und bietet den Nutzern der Plattform somit eine ganz neue Erfahrung. Der Einsatz einer Single-Page-App lohnt sich also grob gesagt in allen Bereichen, in denen großer Wert auf ein intuitives User Interface und hohe Interaktivität gelegt wird. Oft wird schlechte Performance oder fehlende Unterstützung von hardwarespezifischen Features wie dem Zugriff auf das Dateisystem oder die Kamera des Geräts noch als Gegenargument aufgeführt, jedoch ist dies immer abhängig von den speziellen Anforderungen und könnte sich mit Weiterentwicklung der Geräte und der HTML5-Spezifikation bald ändern.

Mit all diesen Möglichkeiten entstehen zwangsläufig auch Probleme und neue Anforderungen an solide Architekturen und Qualitätssicherung dieser webbasierten Software. Die Sprache JavaScript und vor allem das Verstehen ihrer asynchronen und funktionalen Natur spielt dabei eine immer wichtigere Rolle. Viele Konzepte clientseitiger Webentwicklung ähneln denen der klassischen Desktopentwicklung und bekannte Methoden und Programmierparadigmen müssen ggf. überdacht werden. Listing 1 versucht dies anhand eines Vergleichs mit der Sprache PHP zu verdeutlichen. Dieses Beispiel demonstriert die asynchrone Funktionsweise von JavaScript. Es existiert kein sleep, welches aktiv wartet. Moderne Browser erstellen einen JavaScript-Thread pro Browserfenster, JavaScript ist also single-threaded. Wie kann es dann sein, dass der String „Welt“ vor „Hallo“ ausgegeben wird? Browser implementieren dafür einen so genannten „Event Loop“, also eine Art Endlosschleife, die nach Aufsetzen des DOM gestartet wird und in jedem Durchlauf eine Warteschlange (FIFO, First In First Out) registrierter Events abarbeitet. Mittels setTimeout wird ein solches Event registriert und danach kann das Programm erst einmal weiterlaufen, blockiert also nicht. Weitere Events können Benutzerinteraktionen wie Mausklicks sein oder die Antwort auf eine AJAX-Anfrage an einen Server. Dies kann für viele Entwickler, welche lange Zeit mit Sprachen wie Java oder PHP zu tun hatten, vielleicht etwas verwirrend sein und führt oft zu unnötig komplexem Code. Zum Glück existiert mittlerweile ein sehr großes Angebot an Literatur und Werkzeugen für Entwickler, von Frameworks über Dependency Management bis hin zur Qualitätssicherung.

// --- PHP
echo "Hallo ";
sleep(1); // aktives Warten
echo "Welt";
// Ausgabe: "Hallo Welt"

// --- JavaScript
setTimeout(function() {
  console.log('Hallo ');
}, 1000);
console.log('Welt');
// Ausgabe: ?

„Back to the Roots“ – Das MVC Pattern im Web

Bei der Entwicklung von Webanwendungen ist es oft leicht, mit einer Bibliothek zur DOM-Manipulation wie jQuery und einigen Plug-ins sehr schnell etwas auf die Beine zu stellen. Das Problem dabei ist leider oft die Wartbarkeit. Man verirrt sich schnell in einem Wald von Callbacks und Spaghetti-Code, also Code, der unstrukturiert und oft unnötig komplex ist. Ein neues Feature zerschießt ein Altes, von Testbarkeit keine Rede. Soll jedoch auf lange Zeit eine stabile Software entstehen, erfordert dies zuerst eine solide Architektur.Abb. 1 MVC Pattern [2]

Abbildung 1 zeigt das traditionelle MVC-Achitekturmuster. Es basiert prinzipiell auf der Trennung der Daten von deren Präsentation zur Vereinfachung der Entwicklung von Benutzeroberflächen. Benutzerinteraktion wird von einer langlebigen View an einen so genannten Controller weitergeleitet, der die Models manipuliert. Views können sich mithilfe des Observer Patterns für Ereignisse in den Models registrieren und werden bei Änderungen automatisch benachrichtigt. Somit stehen die Views in ständiger Synchronisation mit den darunterliegenden Daten. Models hingegen sind völlig unabhängig, besitzen also keine explizite Abhängigkeit zur View.

Abb. 2: „Model2“ MVC [2]

Zum Vergleich zeigt Abbildung 2 eine Modifikation des MVC-Musters, wie sie oft in serverseitigen Implementierungen in PHP oder Ruby verwendet wird. Der Controller nimmt Benutzerinteraktion entgegen, kommuniziert mit Models, gibt Daten an eine View und rendert diese. Der Unterschied zum traditionellen Muster ist hier die fehlende Beziehung zwischen Model und View.

Wie bereits erwähnt ist das HTTP-Protokoll zustandslos. Views können hier nicht wie im klassischen Muster unmittelbar auf Änderungen in Models reagieren. Somit befindet sich hier auch kein Zustand in einer View. Der Zustand der Applikation wird in der Regel in Session-Cookies verwaltet. Nach dem Ausliefern einer Seite hat der Server den anfragenden Client erst einmal wieder „vergessen“.

MVP und MVVM

Desweiteren erwähnenswert an dieser Stelle sind die Erweiterungen des klassischen MVC-Musters, Model View Presenter (MVP) und Model View ViewModel (MVVM). Sie unterscheiden sich vom traditionellen Muster auch in der fehlenden Beziehung zwischen View und Model. Es ist nicht immer der Fall, dass die JSON-Daten vom Server exakt in die clientseitige Modelrepräsentation passen. Die Daten müssen also ggf. noch vor deren Präsentation auf dem Bildschirm aufbereitet werden. Hier kommt der Presenter oder das ViewModel ins Spiel. Die View selbst sollte hier nur die absolut notwendige Präsentation der Daten beinhalten, während sich die dafür benötigte Geschäftslogik im Presenter befindet. Dies erlaubt vor allem die Testbarkeit des Presenters bzw. ViewModels, unabhängig von der Präsentation.

Der Unterschied zwischen MVP und MVVM besteht eigentlich nur darin, basierend auf Änderungen in den ViewModels mithilfe einer so genannten „Data Binding Engine“ die Views zu aktualisieren. Eines der wesentlichen Ziele von Data Binding ist das Vermeiden von Code, welcher abhängig von der Benutzeroberfläche (hier dem DOM) ist, mit mehr Fokus auf die Geschäftslogik und Testbarkeit. Bekannte Vertreter dieses Konzepts sind KnockoutJS und das AngularJS-Framework von Google.

MV* und JavaScript

Jeder Entwickler, der sich bereits mit einem der MVC-, MVP- oder MVVM-(MV*-)Muster befasst hat, kann diese auch in JavaScript-Apps verwenden. Immer mehr JavaScript-Entwickler erkannten die Vorzüge dieser Muster, was in den letzten Jahren zu einer starken Entwicklung vieler JavaScript-MV*-Frameworks geführt hat. Der Grund für die gute Eignung dieses Musters zeigt sich hauptsächlich in der Anforderung an funktionale und interaktive UI-Elemente. Implementierungen des View Layer lassen sich grob gesagt in zwei Bereiche unterteilen: basierend auf Code (MVP) und basierend auf Markup (MVVM). Listing 2 verdeutlicht diesen Unterschied anhand eines Beispiels: Während bei den „Model-backed Views“ ein Objekt einer View mit einer Referenz auf ein zu observierendes Model existiert, orientiert sich die „Markup-driven“-Implementierung mehr am HTML-Markup. Ein Templating-System kümmert sich dann um das Rendern der Views, und diese haben mithilfe des Frameworks direkten Zugriff auf Variablen.

// "Model-backed Views"
var todoModel = new Todo({title: 'buy milk', completed: false}),
    todoView  = new TodoView({model: todoModel});

// "Markup-driven Views"
{{view TodoView}}
  {{=model.title}}
{{/view}}

Ein mittlerweile äußerst beliebtes MV*-Framework ist Backbone.js [3]. Es orientiert sich eher am MVP-Muster, ist leichtgewichtig und überlässt einige architektonische Entscheidungen dem Entwickler. Eine von Backbone.View abgeleitete View kümmert sich automatisch um das Binden von DOM-Events und kann bei Bedarf auf Ereignisse in relevanten Models reagieren. Mithilfe einer clientseitigen Templatesprache ist es nun möglich, ein Template mit den Daten des Models zu rendern. Diese Art von View erinnert stark an den Presenter im MVP-Muster: auch hier bleibt der funktionale Code der View unabhängig von deren Präsentation und somit gut testbar. Besonders wichtig ist auch das Verstehen von Zuständen. Views einer Single-Page-App „leben“ länger als Views bzw. die Seiten einer klassischen Webanwendung. JavaScript-Objekte befinden sich also für längere Zeit im RAM des Clients.

Die neue Rolle des Servers

In serverseitigen Webanwendungen kommt oft das sog. Front-Controller Pattern zum Einsatz. Der Webserver nimmt eine Anfrage entgegen und leitet sie an einen immer gleichen Eintrittspunkt, z. B. eine index.php. Nach Initialisierung notwendiger Komponenten werden anhand der URL-Segmente einer Anfrage entschieden, welcher Controller geladen werden muss, um eine bestimmte Aktion auszuführen und anschließend die richtige View zu rendern. Dieser Prozess wird auch „Routing“ genannt. Die steigende Komplexität clientseitiger Software führt dazu, dass mittlerweile sehr viel dieser Logik ebenfalls auf dem Client benötigt wird.

Die Software muss auf dem Client also auf zwei Aktionen reagieren können: DOM-Events wie z. B. ein Klick auf einen Button und Hash bzw. PushState Changes beim Navigieren zwischen Seiten. Technologien wie das HTML5-History-API und PushState ermöglichen das Verwalten verschiedener Zustände sowie das Manipulieren des URLs des Browsers. Der Server liefert einmalig alle statischen Dateien wie HTML, CSS und JavaScript-Code aus und dient danach nur noch als API. Diesem ist dabei egal, wie das URL-Segment einer Anfrage aussieht: Für die Segmente „/“ sowie „/contacts“ würde dasselbe Dokument ausgeliefert werden, der JavaScript-Code übernimmt dann das Routing und Rendering. Im Idealfall sollten die clientseitigen Routen wo notwendig in Synchronisation mit den serverseitigen Routen stehen; und neben dem immer selben statischen HTML-Dokument und JavaScript-Code kann man ggf. schon Daten und Markup mithilfe einer serverseitigen Templatesprache rendern, abhängig vom Anfrage-URI. Dies kann zu einer Steigerung der Performance führen und löst das „Bookmarking“-Problem reiner JavaScript-Anwendungen: Jeder Link der Web-App muss einen bestimmten Zustand der Anwendung repräsentieren. Mithilfe des Node.js-Projekts ist es sogar möglich, denselben JavaScript-Code auf Client und Server zu verwenden.

Modulare Architektur

Das schlimmste Feature der Sprache JavaScript ist seine Abhängigkeit von globalen Variablen [4]. Globale Variablen können zum Alptraum in großen Softwareprojekten werden; unter anderem weil sie zur Laufzeit von jedem Teil des Programms verändert werden können, woraus stark fehlerhaftes Verhalten der Software resultieren kann. Das Einhalten von Coding-Standards und Einsetzen von Tools zur statischen Codeanalyse wie JSHint können hier sehr hilfreich sein. Um globale Variablen zu vermeiden, bedienen sich Entwickler an einem der schönen Features von JavaScript: Funktionen. Zwei dieser Konzepte nennen sich „Self-executing anonymous function“ (SEAF, Listing 3) und „Immediately Invoked Function Expression“ (IIFE, Listing 3). Die Sichtbarkeit von Variablen gilt in JavaScript pro Funktion, zuvor außerhalb deklarierte Variablen sind innerhalb jedoch ebenfalls sichtbar. Alle außerhalb von Funktionen deklarierten Variablen sind automatisch Teil des globalen Namensraums bzw. des globalen Objekts window. Listing 3 bedient sich dem Konzept der IIFE und schlägt gleich mehrere Fliegen mit einer Klappe: Zum einen wird das Verschmutzen des globalen Namensraums deutlich minimiert, denn die einzige globale Variable ist hier myApp. Der Variable myApp wird die Rückgabe der sich selbst ausführenden Funktion mit dem Namen MyApp zugewiesen. Es wird eine öffentliche Schnittstelle in Form eines Objekts zurückgegeben. Dieses Objekt besitzt hier nur eine Methode: getSecret().

// "Self-executing anonymous function"
(function (window) {
  // code
}(this));


// "Immediately Invoked Function Expression"
var myApp = (function MyApp(window) {
  var secret = 42;
  return {
    getSecret: function () {
      return secret;
    }
  };
}(this));

myApp.getSecret(); // 42

Außerdem zeigt dieses Beispiel eine mögliche Implementierung von Information-Hiding in JavaScript. Die Variable secret ist „private“, also von außen nicht sichtbar. Dennoch wird der globale Namensraum verschmutzt. Oft wurde eine globale Variable wie MY_APP angelegt, welche darunter weitere Module deklariert, z. B. MY_APP.Todos.fetch(). Das ist aber weder sehr elegant, noch auf lange Zeit tragbar. Auch das Verwalten der Reihenfolgen beim Einbinden der Skripte kann umständlich werden.

Modularität bedeutet das entkoppelte Auslagern von Funktionalität in Modulen. Leider unterstützt die aktuelle ECMAScript-Spezifikation (ECMA-262) keinen Mechanismus zum Verwalten von Modulen wie man ihn von anderen Sprachen wie Java oder Python kennt. Aus diesem Grund existieren eigene Entwicklungen bezüglich eines Standards, CommonJS [5] und die Asynchronous Module Definition (AMD) [6], um Teile des Codes in leicht wartbaren und unabhängigen Modulen zu organisieren. Dies hält den Code gut strukturiert und besser wartbar, vor allem in größeren Projekten. CommonJS wird eher in Umgebungen außerhalb des Browsers wie z. B. Node.js oder Titanium Appcelerator implementiert. Die bekannteste Implementierung der AMD für Browser ist Require.js, ein Tool zum asynchronen Laden von Ressourcen wie JavaScript und CSS. Module können definiert werden und Require.js kümmert sich um das automatische Auflösen der Abhängigkeiten dieser Module. Listing 4 zeigt die unterschiedliche Syntax von AMD und CommonJS.

// ========== 1. CommonJS ==========
// Definition eines Moduls (my-module.js)
var myModule = {
  doSmt: function () {
    return 0;
  }
};
// exportiere die öffentliche Schnittstelle dieses Moduls
module.exports = myModule;

// Verwendung
var myModule = require('./path/to/my-module');
myModule.doSmt();

// ========== 2. AMD ==========
// Definition eines Moduls (my-module.js)
define('module-id', [/*Abhängigkeiten*/], function () {
  // exportiere die öffentliche Schnittstelle dieses Moduls
  return {
    doSmt: function () {
      return 0;
    }
  }
});

// Verwendung
require(['module_id_or_path', ...], function (myModule, ...) {
  myModule.doSmt();
});

Modularität spielt also eine sehr wichtige Rolle, wenn es um Wart- und Testbarkeit oder auch verteiltes Arbeiten geht. Mit der AMD wurde ein System geschaffen, welches es ermöglicht, JavaScript-Code im Browser in Module aufzuteilen und diese asynchron zu laden. Jedes Modul ist gekapselt und kümmert sich im Idealfall nur um eine Aufgabe. Module können auch für Tests geladen und Abhängigkeiten dort leicht ausgetauscht werden. Außerdem können Entwickler schnell nachvollziehen, wo welches Modul verwendet wird.

Werkzeuge moderner Frontend-Entwicklung

Durch die in den letzten Jahren rapide gestiegene Beliebtheit der Sprache JavaScript hat sich eine große Community mit starkem Interesse an Open Source und der Förderung neuer und alter Konzepte der Softwareentwicklung gebildet. Und wie überall in der Softwareentwicklung ist es auch hier meistens schlauer, das Rad nicht neu zu erfinden, sondern auf Lösungen zu setzen, die bereits erprobt und von größerer Masse akzeptiert wurden.

Testing und Debugging

Testing und Debugging sind zwei Fähigkeiten, die zum Standardarsenal eines Webentwicklers gehören sollten. Werkzeuge wie Firebug oder die ChromeDevTools erlauben das Debuggen von JavaScript-Code im Browser, also das Setzen von Breakpoints und schrittartiges Durchgehen des Codes. Man kann u. a. Netzwerkaktivitäten analysieren oder via console.log() Nachrichten des Programms loggen.

Wie Abbildung 3 zeigt, ist unter vielen JavaScript-Entwicklern die Motivation bezüglich des Testens ihrer Software leider noch eher gering. Soll jedoch stabile und qualitativ hochwertige Software entstehen, ist testgetriebene Entwicklung unverzichtbar. Bekannte Tools sind u. a. QUnit, Mocha oder Jasmine.

Abb. 3: Ein großer Teil von JavaScript Entwicklern testet überhaupt nicht [7]

Frameworks

Moderne JavaScript-Frameworks können enorm bei der Strukturierung der Software helfen und fördern eine von Anfang an saubere und gut organisierte Basis. Als gutes Beispiel zur Demonstration der Funktionsweisen unterschiedlicher Frameworks eignet sich die „To-do-App“. Mit dieser App können Benutzer eine Liste zu erledigender Aufgaben verwalten, darin Aufgaben anlegen, aktualisieren und löschen. Leider – oder zum Glück? – herrscht ein so großes Angebot an Frameworks, dass es für Entwickler nicht immer leicht ist, diese angemessen zu vergleichen. Das Projekt TodoMVC [8] kann hier sehr hilfreich sein. Ziel dieses Projekts ist es, Entwicklern bei der Entscheidung für ein JavaScript-MV*-Framework zu helfen, was mithilfe verschiedener Implementierungen ein und derselben To-do-App demonstriert wird. Es vereint alle wichtigsten und bekanntesten Frameworks und es lohnt sich auf jeden Fall, einige der Implementierungen einmal durchzugehen und zu vergleichen. Clientseitiges Routing und auch die vier grundlegenden Datenbankoperationen Create, Read, Update und Delete (CRUD) werden vorgestellt. Es existiert sogar eine Implementierung in reinem JavaScript, sowie eine nur auf jQuery basierend.

Dependency Management

Webprojekte haben oft mehrere Abhängigkeiten an externe Bibliotheken wie z. B. jQuery. All diese Abhängigkeiten manuell zu verwalten und aktuell zu halten, kann je nach Größe des Projekts problematisch sein. Aus diesem Grund existieren Package Manager wie npm für Node oder Composer für PHP auch für die clientseitige Entwicklung mit JavaScript. Bekannte Tools sind u. a. Jam [9] und Bower von Twitter [10]. Ähnlich wie bei npm oder Composer wird auch hier in der Regel eine JSON-Datei im Root-Verzeichnis des Projekts angelegt, welche die externen Abhängigkeiten definiert. Diese können neben JavaScript auch Bilder oder CSS beinhalten.

Build-System

Mit dem Begriff „Build-System“ ist der Vorgang der Optimierung von statischen Assets für den produktiven Einsatz gemeint. Darunter fallen z. B. Bilder, CSS- sowie JavaScript-Code. Gängig ist dabei die Konkatenation der Inhalte mehrerer CSS- und JavaScript-Dateien zu je einer CSS- und einer JavaScript-Datei. Dies minimiert die Anzahl der HTTP-Anfragen, welche das Laden der Seiten verlängern können. Weitere mögliche Optimierungsmethoden sind die verlustfreie Kompression von Bildern oder das Minimieren von HTML. Bekannte Tools sind u. a. der YUI Compressor, der Google Closure Compiler oder der Require.js Optimizer.

Es existiert ein sehr großes und vielfältiges Angebot an Werkzeugen, die versuchen, Entwickler dabei zu unterstützen, die alltäglichen Probleme moderner Webentwicklung besser zu meistern. „A fool with a tool is still a fool“ – setzt man jedoch sein Werkzeug richtig ein, kann es die Arbeit deutlich erleichtern und auch zu einem stabileren und besseren Produkt führen.

Beispiel: Übung macht den Meister und gute Software zu entwickeln bedeutet „Learning by doing“. Das TodoMVC-Projekt eignet sich hervorragend zur Demonstration einer Single-Page-App. Ich habe einen Fork des Projekts bei GitHub erstellt und dort das Beispiel mit Backbone.js und Require.js erweitert [11]. Require.js wird zur Modularisierung verwendet und Backbone.js ist im Vergleich zu anderen Frameworks vielleicht leichter zu verstehen und deshalb für Einsteiger besser geeignet.

Nach aktuellem Stand (März 2013) fehlen im TodoMVC-Projekt noch Tests. Aus diesem Grund habe ich eine Testumgebung aufgesetzt, bestehend aus einigen interessanten Werkzeugen zum Testen von JavaScript-Apps. Außerdem wurde JSHint und der Require.js Optimizer zur Demonstration des Automationstools Grunt integriert. Dies erlaubt das automatisierte Optimieren des Codes der TodoMVC-App für den produktiven Einsatz. Weitere Informationen finden sich in der readme.md unter [11].

[ header = Qualitätssicherung ]

Qualitätssicherung

Wie hält man Komplexität im Zaum und was kann man tun, um die Qualität komplexer Softwareprodukte zu steigern? Was die Qualitätssicherung von JavaScript-Software angeht, ist die Motivation im Vergleich zu Sprachen wie Java oder PHP bis jetzt noch eher gering. Mit der steigenden Nachfrage nach webbasierter Software scheint sie jedoch stark zuzunehmen. Auch hier existiert mittlerweile ein großes Set an Tools zur Verbesserung und Vereinfachung des Qualitätssicherungsprozesses.

Testgetriebene Entwicklung

Testgetriebene Entwicklung spielt eine äußerst wichtige Rolle bei der Entwicklung hochqualitativer Software. Nicht nur weil dadurch Fehler frühzeitig entdeckt oder Refactoring und Wartung stark erleichtert werden. Testgetriebene Entwicklung setzt eine völlig andere Denkweise voraus. Tests werden geschrieben, bevor man den Code schreibt. Man macht sich also automatisch von Anfang an mehr Gedanken über die öffentlichen Schnittstellen seiner Module und Abhängigkeiten zwischen Objekten. Die so genannte verhaltensgetriebene Entwicklung erweitert dieses Konzept: Softwaretests sollen basierend auf der Anforderungsspezifikation geschrieben werden. Tests sind also die ausführbare Version der Spezifikation und somit auch eine Art von Dokumentation. Listing 5 verdeutlicht dies unter Verwendung des Testframeworks Mocha. Wird dieser Test ausgeführt, so wird klar, was damit gemeint ist. Ein describe-Block definiert einen gewissen Rahmen, hier das To-do-Model. Darin können beliebig viele it-Blöcke folgen, die dann eine bestimmte Anforderung bzw. das zu erwartende Verhalten der Software beschreiben und testen:

Todo Model   soll dies tun.......  Test OK   soll das können.....  Test OK usw.
describe('Todo Model', function () {

  // ...

  it('should validate the title attribute', function (done) {
    var todo = new Todo();

    todo.on('error', function () {
      // Sobald versucht wird das Model zu verändern und die Validierung fehlschlägt, wird dieser Callback ausgeführt. "done" signalisiert dann das erfolgreiche Ende dieses Tests
      done();
    });

    todo.set({
      title: ''
    });
  });
});

Das ermöglicht auch das bessere Verstehen der Tests bzw. der Software im Allgemeinen von Stakeholdern oder weiteren, nicht technischen Mitarbeitern. Außerdem konzentrieren sich diese Art von Tests mehr auf das externe Verhalten eines Moduls. Dies erleichtert späteres Refactoring und Optimierungen unter der Haube sehr, denn die Tests lassen den Entwickler wissen, wenn etwas nicht mehr stimmt. Das Beispiel demonstriert auch den schönen Umgang von Mocha mit asynchronen Tests. Eine Callback-Funktion (hier done) kann aufgerufen werden, um das Ende eines Tests auszulösen. Das ist besonders hilfreich beim Testen von asynchronem Code wie AJAX Calls oder Animationen.

In meinem TodoMVC-Fork unter [11] befindet sich ein Beispiel mit Mocha zur Demonstration der Schwerpunkte beim Testen von Single-Page-Apps. Die Testsuite nutzt auch Require.js zum Laden der zu testenden Module. Diese können neben Models oder eigenen Helper-Funktionen auch Router oder sogar Views sein. Alle relevanten Komponenten der Software können also in Isolation getestet werden.

Testautomatisierung

Wichtig bei der Entwicklung mit JavaScript ist auch das Testen in möglichst vielen Browsern. Doch Entwickler sind faul und wollen ihre Tests nicht immer wieder manuell im Browser öffnen müssen. Werkzeuge wie JSTestDriver oder testem ermöglichen das automatisierte Ausführen unserer Tests in beliebig vielen, vorher konfigurierten Browsern. Somit kann man diese sogar in den Build-Prozess eines Continous-Integration-Servers wie Jenkins einhängen und bei jedem Commit eines Entwicklers laufen die Tests in allen Browsern durch. Unter [11] findet sich ein Anwendungsbeispiel unter Verwendung von testem.

Funktionaltests

Menschen machen Fehler. Natürlich ist es essenziell, Software auch von Menschen testen zu lassen, jedoch sollte das automatisierte Testen von Benutzerinteraktion nicht unbeachtet bleiben. Mithilfe von Werkzeugen wie Selenium oder PhantomJS lassen sich Use Cases relativ genau in ausführbarer Form abbilden. Ein Beispiel dafür könnte das Ausfüllen eines Formulars sein, um die clientseitige Validierung in Aktion zu testen. Der Test kann möglichst viele Möglichkeiten des Ausfüllens durchspielen und ist dann immer wiederholbar, auch nach späteren Änderungen. Es gibt sogar Möglichkeiten, Selenium mit PhantomJS zu verbinden. Auch hier lohnt sich ein Blick auf [11].

Statische Codeanalyse

Für die statische Analyse von JavaScript-Code gibt es das Tool JSHint. JSHint scannt den Quellcode und spuckt Warnungen aus, falls es Probleme entdeckt. Diese Probleme können die versehentliche Verwendung undefinierter Variablen oder auch das Nichteinhalten von Coding-Standards sein. Man kann Regeln global mithilfe einer Datei .jshintrc und auch pro Datei definieren. JSHint lässt sich auch in IDEs wie z. B. PhpStorm integrieren. Wer ganz sicher sein will, kann lokal einen Pre Commit Hook anlegen, der vor jedem Commit JSHint laufen lässt. Somit gelangt nur Code ins Repository, der gemäß JSHint „sauber“ ist.

Auslieferungsgeschwindigkeit

Die Auslieferungsgeschwindigkeit spielt eine äußerst wichtige Rolle, vor allem bei der Entwicklung für mobile Geräte mit niedrigen Übertragungsraten. Kein Benutzer will lange warten. Für den produktiven Einsatz empfiehlt es sich deshalb, die Anzahl an statischen Daten wie JavaScript- und CSS-Dateien so gering wie möglich zu halten oder ganz auf ein Content Delivery Network (CDN) auszulagern – einen „Server“, der allein für das Ausliefern statischer Daten optimiert ist. Jede externe Abhängigkeit erfordert eine weitere HTTP-Anfrage an den Server und verlangsamt somit das initiale Laden der App. Diese CSS- und JavaScript-Dateien werden dann vom Browser gecacht und müssen bei erneutem Besuch der Website nicht nochmal heruntergeladen werden, sofern kein neues Release durchgeführt wurde. Um den Vorgang der Optimierung zu automatisieren, existieren zahlreiche Tools. Unter [11] befindet sich ein Beispiel zum Optimieren des JavaScript-Codes der TodoMVC-App mithilfe des Require.js Optimizers. Der gesamte Code wird zu einer Datei zusammengefasst und aus den HTML-Templates werden AMD-Module erstellt, die den HTML-Code dann in Form einer JavaScript-Variable zurückgeben. Während der Entwicklung werden die HTML-Dateien also per AJAX geladen, später sind sie dann nichts weiter als JavaScript-Strings, die wir mit Daten rendern und in den DOM einfügen können. Projekte wie beispielsweise das HTML5-Boilerplate [12] sind sehr bemüht, die Optimierung von Webseiten zu fördern. Es existieren auch zahlreiche Tipps und Best Practises bezüglich Maßnahmen zur serverseitigen Optimierung wie z. B. der gzip-Kompression. Ein Blick auf deren Webseite lohnt sich.

Fazit

Die Webentwicklung schnellt rasend voran. Was heute der letzte Schrei ist, wird morgen vielleicht kaum mehr angesprochen. Java Applets haben sich nicht durchsetzen können und viel zu lange Zeit waren Entwickler abhängig von Browser-Plug-ins wie Flash, um so selbstverständliche Funktionen wie das Abspielen von Musik zu realisieren. HTML5 wird mittlerweile von vielen großen Konzernen unterstützt und eingesetzt. Technologien wie das -Element oder die neuen Media-APIs (getUserMedia, WebRTC etc.) eröffnen so viele Möglichkeiten im Browser, dass es irgendwann vielleicht keine Rolle mehr spielt, ob App oder „Web-App“. App ist App – Hauptsache sie erfüllt ihren Zweck und sieht gut aus. In Mozillas Firefox OS beispielsweise ist jede Website eine App. Und mit Tools wie PhoneGap bzw. Apache Cordova kann man eine existierende Single-Page-App nehmen und sie als native App auf mobile Geräte portieren.

Browser und deren JavaScript Engines werden stetig weiterentwickelt und optimiert. Und von der Entwicklung von Kommandozeilentools oder serverseitiger Software via Node.js bis hin zur Entwicklung von Desktop- oder Mobile-Applikationen mit Werkzeugen wie Titanium Appcelerator ist alles möglich. Sogar Windows-Apps lassen sich jetzt mit JavaScript erstellen. Es sieht also gut aus für die Sprache des Web.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -