Suche
Kolumne

EnterpriseTales: Single-page Applications – Klarheit schaffen

Sven Kölpin

©S&S Media

Zugegebenermaßen kann der Einstieg in die Single-Page-Welt verwirrend sein. Noch vor der ersten Zeile Code müssen verschiedene Tools installiert und unzählige Eingaben in der Kommandozeile getätigt werden. Zwar bieten viele Single-Page-Frameworks mittlerweile Werkzeuge (Angular CLI oder Create React App), mit deren Hilfe sich Projekte größtenteils automatisiert erstellen und konfigurieren lassen. Trotzdem bleibt bei Neulingen oft ein fader Beigeschmack: Braucht man das wirklich alles? Früher musste man doch nur jQuery herunterladen …

Rein technisch gesehen zeichnet sich eine Single-Page Application (SPA) heute vor allem dadurch aus, dass das HTML der Benutzeroberfläche erst im Browser mithilfe von JavaScript erzeugt wird (clientseitiges Rendering). Dadurch kann ein Großteil der UI-Logik, inklusive des Renderingprozesses, vom Server in den Client verlagert werden. Eine SPA fühlt sich für Benutzer während des Betriebs deshalb häufig flüssiger an als serverseitig gerenderte Webanwendungen. Listing 1 zeigt die Umsetzung einer zugegebenermaßen sehr rudimentären SPA. Um den Code zu vereinfachen, wird hier jQuery verwendet. Die Abfrage von JSON-Daten findet über eine HTTP-basierte Schnittstelle statt. Die Daten werden in der renderApp-Methode in HTML-Fragmente übersetzt, also clientseitig gerendert. Unter der Haube machen das, zumindest in Ansätzen, auch die aktuellen SPA-Librarys und -Frameworks so. Dort kommen jedoch ausgefeilte und performante Konzepte wie Virtual-DOM-Strukturen und Templating Engines zum Einsatz.

class TwitterApp {
  async init() {
    this.tweets = await $.get('http://twitter.com/tweets');
    this.renderApp();
  }

  renderApp() {
    const items = this.tweets.map(tweet => $(`<li>${tweet}</li>`));
    $(document.body).append($('<ul/>').append(items));
  }
}
new TwitterApp().init();

Zugrunde liegt Node.js

Die Basis für die moderne Webentwicklung bildet Node.js. Die Plattform ist dadurch bekannt geworden, dass sie in immer mehr großen Unternehmen für die Umsetzung leichtgewichtiger und skalierbarer Server verwendet wird. Dabei ist Node.js aber in erster Linie kein Serverframework, sondern eine JavaScript Runtime. Damit lässt sich JavaScript lokal und ohne die Benutzung eines Browsers ausführen. Auch der Zugriff auf das Dateisystem oder das Erstellen von HTTP(S)-Servern ist möglich. Das ist bei der Entwicklung einer SPA hilfreich, weil sich beispielsweise das Ausführen von Tests, statische Codeanalysen oder andere Build-Schritte über die Kommandozeile anstoßen lassen. Auch die CLI-Tools sind in der Regel in JavaScript geschrieben und benötigen zur Ausführung deshalb Node.js.

Zusätzlich zu Node.js gibt es noch den Node Package Manager (npm), ein Dependency-Management-System für JavaScript-Projekte, das das Hinzufügen und Verwalten von Softwarepaketen über die Konsole ermöglicht. Ein Online-Repository bietet Entwicklern Zugriff auf unzählige JavaScript Librarys und Frameworks, die sich beispielsweise für das Erstellen von SPAs oder die serverseitige Entwicklung verwenden lassen.

Die mit npm installierten Abhängigkeiten werden in einer Art Manifestdatei hinterlegt, der package.json. Jedes npm-Projekt enthält eine solche Datei, in der neben den Abhängigkeiten noch Metainformationen wie der Projektname oder die Projektversion hinterlegt sind. Auch lassen sich dort Skripte definieren, die man zum Ausführen verschiedener Build-Schritte nutzen kann (Listing 2).

{
  "name": "myapp",
  "version": "1.0.0",
  "description": "It can do stuff",
  "scripts": {
    "test": "jest",  //“npm test“ runs tests
    "lint": "eslint ."  //“npm run lint“ executes linting
    "build": "npm test && npm run lint" //executes both
  },
  "author": "Renke10",
  "license": "MIT",
  "dependencies": {
    "jquery": "3.2.1"  //only dependency needed by our app
  },
  "devDependencies": {
    "eslint": "4.5.0",  //dependencies only for development
    "jest": "20.0.4"
  }
}

JavaScript übersetzen

Dem aufmerksamen Leser mag aufgefallen sein, dass der in Listing 1 gezeigte Quellcode nur noch wenig an den JavaScript-Code aus früheren Zeiten erinnert. Viele der hier verwendeten Sprachfeatures („Exploring JS“ oder „Exploring ES2016 and ES2017„) sind noch recht jung, gehören aber zur aktuellen Version von JavaScript (ECMAScript 2017). ECMAScript (ES) ist die Spezifikation hinter JavaScript und wird stetig weiterentwickelt. Seit Version ES 2015 besteht das Ziel, jährlich eine neue Version zu veröffentlichen. Die Syntax von JavaScript wurde deshalb in vergleichsweise kurzer Zeit stark erweitert.

Die Hersteller von JavaScript Engines müssen die Sprachneuerungen natürlich erst umsetzen, bevor man sie in den Browsern verwenden kann – und das kann dauern. Würde man das Beispiel aus Listing 1 in einer aktuellen Version des Chrome-Browsers (ab Version 60) ausführen, wäre alles okay. Im Internet Explorer hingegen wird das nicht funktionieren, weil die dahinterstehende JavaScript Engine die aktuelle ECMA-Spezifikation nicht ausreichend unterstützt. Auch andere Browserversionen verstehen noch längst nicht alle neuen Features. Um trotzdem nicht auf die aktuellen und offen gesagt längst überfälligen Sprachbestandteile von JavaScript verzichten zu müssen, gibt es Transpiler (auch JavaScript-Compiler oder nur Compiler).

Mithilfe eines Transpilers lassen sich neue Versionen von JavaScript in ältere übersetzen. So lassen sich aktuelle ECMAScript-Sprachfeatures verwenden, ohne dass man dabei eine umfassende Browserunterstützung verliert. In den meisten Transpiler-Werkzeugen lässt sich einstellen, in welche Sprachversion der jeweilige Code zurückübersetzt werden soll. Listing 3 zeigt eine nach ES 5 transpilierte Version der uns bereits bekannten TwitterApp-Klasse. Diese Version ließe sich jetzt auch vom Internet Explorer (ab Version 9) interpretieren, weil hier wieder klassische Funktionen und Variablen verwendet werden.

Lesen Sie auch: Angular 5 vs. React – Wer hat wo die Nase vorn?

Der bekannteste Transpiler ist Babel. Babel wird in vielen JavaScript-Projekten verwendet und bietet ein umfassendes Feature- und Toolset. Außerdem gibt es noch den TypeScript-Compiler, der zwar kein Transpiler im klassischen Sinne ist, es im Grunde genommen aber ebenfalls ermöglicht, eine moderne Sprache (TypeScript) in eine vom Großteil der Browser verständliche ECMA-Version zu überführen. Die Transpiler-Werkzeuge werden in der Regel über npm installiert und mit Node.js ausgeführt.

Das Transpilieren hat aber natürlich Grenzen. So wird im Mittel mehr Code erzeugt (Listing 3), und es können selbstverständlich nicht alle Features übersetzt werden. Grundsätzlich gehört dieser Schritt aktuell jedoch zum Standardworkflow bei der Entwicklung einer jeden SPA. Denn nur so ist deren zeitgemäße Entwicklung möglich, ohne dabei eine umfassende Browserunterstützung und damit Benutzergemeinde zu verlieren.

// ECMAScript 2017
class TwitterApp {
}

// Transpiled to ECMAScript 5
function _classCallCheck(i,c) {...}

  var TwitterApp = function TwitterApp() {
    _classCallCheck(this, TwitterApp);
  };

Kein Spaghetticode mehr

Seit 2015 sind auch ECMAScript-Module Bestandteil der JavaScript-Sprachspezifikation. Damit haben Entwickler nun endlich die Möglichkeit, Abhängigkeiten zwischen JavaScript-Dateien standardisiert durch Import- und Exportdeklarationen zu beschreiben (Listing 4). Vorher war das nur mithilfe von externen Bibliotheken und komplizierten Patterns möglich. Allerdings hat das Ganze momentan noch zwei Haken: Erstens ist zwar die Syntax der Module bereits seit Version ES 2015 definiert, es fehlt aber bislang der browserübergreifende Support. Zum aktuellen Zeitpunkt gibt es, abgesehen vom Safari-Browser, bei den Browsern allenfalls eine experimentelle Unterstützung. Zweitens macht uns aktuell noch das Übertragungsprotokoll einen Strich durch die Rechnung. Jedes importierte JavaScript-Modul wird vor der Ausführung wie eine normale JavaScript-Datei heruntergeladen. Bei großen Anwendungen wirkt sich das natürlich negativ auf die Seitenladezeit aus, weil sehr viele Netzwerkanfragen getätigt werden müssen. Ohne die umfassende Verbreitung des HTTP/2-Protokolls, das es unter anderem ermöglicht, mehrere Anfragen parallel über eine Verbindung abzuhandeln, lassen sich native JavaScript-Module also momentan nicht ohne Performanceeinbußen verwenden. Um trotzdem bereits heute mit ECMAScript-Modulen arbeiten zu können, gibt es Module Bundler.

index.js
//import paths are relative. The //file ending can be omitted
import {sum} from './math';

//all imported function sum
console.log(sum(1, 2));

Math.js
//this file exports the function “sum“
export const sum = (a, b) => a + b;

Pakete schnüren

Module Bundling ermöglicht es, aus vielen verschiedenen JavaScript-Dateien eine oder mehrere finale Dateien zu generieren, in der alle Abhängigkeiten zusammengefasst und in der richtigen Reihenfolge eingefügt sind (Abb. 1). Mithilfe dieser Technik können JavaScript-Projekte bereits ECMAScript-Module für die Codeorganisation verwenden, während an die Browser trotzdem nur transpilierte und zusammengefasste JavaScript-Dateien ausgeliefert werden. So lässt sich der aktuell fehlende Browsersupport für Module überbrücken. Zusätzlich wirkt sich das bei der Verwendung von HTTP 1.1 positiv auf die Seitenladezeit aus, weil weniger Netzwerkanfragen notwendig sind.

Der derzeit am weitesten verbreitete Bundler ist webpack. Er wird beispielsweise als fester Bestandteil von Angular (ab Version 2) oder in den meisten React-Projekten verwendet. Ähnlich wie die Transpiler lassen sich auch die meisten Bundler über npm installieren und werden über Node.js ausgeführt.

Module Bundler können aber häufig noch viel mehr als nur JavaScript-Dateien zu vereinen. Beispielsweise lässt sich während des Bundling-Vorgangs automatisiert ungenutzter Code entfernen (Dead Code Eliminiation), oder es kann eine Komprimierung des Codes vorgenommen werden. Das umfasst z. B. das Entfernen nicht benötigter Zeichen und das Kürzen von Variablennamen. Diese Prozesse reduzieren die Menge des auszuliefernden Codes zwar schon deutlich, dennoch können auch die zusammengefassten und komprimierten JavaScript-Dateien noch immer eine beachtliche Größe erreichen. Viele Tools bieten deswegen zusätzlich die Möglichkeit, ein Bundle in kleinere Unterpakete aufzuteilen (Code Splitting). So können Teile des Codes erst bei Bedarf nachgeladen und die initiale Seitenladezeit kann reduziert werden, z. B. durch NgModules bei Angular ab Version 2.

Abb. 1: Module Bundling

Abb. 1: Module Bundling

Mit Typsicherheit arbeiten

JavaScript ist dynamisch typisiert. Das ist einer der Hauptgründe, warum die Sprache von vielen Entwicklern verteufelt wird. Man muss aber auch als JavaScript-Fan zugeben, dass die fehlende Typisierung spätestens bei größeren Projekten Kopfschmerzen bereiten kann. Refactorings sind dann beispielsweise nur noch mit einer extrem hohen Testabdeckung gefahrlos möglich. Dazu kommt eine durch die fehlende Typisierung eher mittelmäßige IDE-Unterstützung von JavaScript. Genau hier helfen TypeScript von Microsoft und Flow von Facebook weiter. Beide ermöglichen es, herkömmlichen JavaScript-Code mit Typen zu annotieren und so den Code während der Entwicklung verständlicher und vor allem typsicher zu machen. TypeScript ist dabei als eigene Sprache zu verstehen, die sogar noch zusätzliche Features liefert (z. B. Class Properties oder Variablensichtbarkeit). Flow hingegen ist eine Art Sprachaufsatz, der keine neuen Features bringt, sondern ausschließlich für die statische Typsicherung genutzt wird. Beide Tools werden in großen Projekten eingesetzt und genießen eine umfassende Unterstützung in der Community. Auch die Syntax unterscheidet sich nur wenig (Listing 5).

Es ist wichtig zu verstehen, dass der typsichere Code, egal ob von Flow oder TypeScript, nicht direkt von den JavaScript Engines interpretiert werden kann. Diese unterstützen schließlich nur ECMA-Script-kompatiblen Code, und der ist noch immer dynamisch typisiert. Deshalb müssen jegliche Typinformationen, und im Fall von TypeScript alle nicht standardisierte Sprach-Features, vor dem Ausliefern an den Browser wieder entfernt oder transpiliert werden. Das passiert in dem uns bereits bekannten Schritt der Transpilierung, bei dem aus TypeScript- und Flow-Code wieder ECMAScript-kompatibler Code generiert wird.

Flow:
//@flow
const sum = (a: number, b: number) => a + b;

TypeScript:
const sum = (a: number, b: number) => a + b;

Fazit

Frei nach dem Motto „Was nicht passt, wird passend gemacht“ wurde in den letzten Jahren versucht, die Schwächen der Plattform Web mit zahlreichen Tools und Workarounds zu besiegen. Zwar hat das relativ gut funktioniert, die Folge sind jedoch eine vergleichsweise hohe Komplexität im Tooling und ein dadurch erschwerter Einstieg in die Welt der SPAs. Glücklicherweise werden aber nicht nur Workarounds für technische Hindernisse erfunden, sondern es findet auch eine stetige Verbesserung der Plattform an sich statt. Wichtige Punkte sind dabei die kontinuierliche Weiterentwicklung der Programmiersprache JavaScript und die Verbesserung der Webbrowser, die mit zahlreichen innovativen APIs (z. B. Service Worker) dazu beitragen, das Web als Applikationsplattform zu manifestieren. Das Ende der Fahnenstange ist hier noch lange nicht erreicht und die Zukunft der JavaScript-, Web- und damit Single-Page-Welt bleibt extrem spannend. In diesem Sinne: Stay tuned!

Geschrieben von
Sven Kölpin
Sven Kölpin
Sven Kölpin ist Enterprise Developer bei der open knowledge GmbH in Oldenburg. Sein Schwerpunkt liegt auf der Entwicklung webbasierter Enterprise-Lösungen mittels Java EE.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.