Single-Page-Anwendungen Framework-unabhängig entwickeln

Beim Entwickeln einer Single-Page App (SPA) haben Programmierer die Qual der Wahl, wenn es um die geeignete Technik geht. Nicht immer ist ein Framework zwingend notwendig.

In Pocket speichern vorlesen Druckansicht 13 Kommentare lesen
Single-Page-Anwendungen Framework-unabhängig entwickeln
Lesezeit: 24 Min.
Von
  • René Viering
Inhaltsverzeichnis

Der Unterschied zwischen klassischen, serverseitig gerenderten Webseiten und Single-Page Apps liegt hauptsächlich in der geänderten Rolle des Servers. Während er ursprünglich neben dem Ausliefern der Ressourcen (HTML, CSS, JS) auch das komplette Rendering übernahm, ist er heute primär ein Datenlieferant (in Form von JSON) und stellt nur noch eine initiale Seite zur Verfügung. Auf ihr beginnt die Single-Page App ihre Arbeit. Navigiert ein User auf der Website, fordert die Anwendung Daten vom Server an und rendert diese anschließend dynamisch auf dem Client. Der übliche Roundtrip zum Server entfällt. Geblieben ist allerdings seine Rolle während der Authentifizierung.

Jeder Entwickler kennt sicherlich die folgende Situation: Ein neues SPA-Framework steht in den Startlöchern und es entsteht innerhalb der Community der Eindruck, es sei sofort auf den Zug aufzuspringen. Um feststellen zu können, ob sich der Umstieg lohnt, ist es unerlässlich, sich näher mit den Konzepten hinter Single-Page Apps zu beschäftigen. Im Kern lassen sich diese auf die folgenden Grundpfeiler zurückführen, die in den nächsten Abschnitten Thema sind:

Grundpfeiler (Abb. 1)

Der Client fordert die Webseite über einen HTTP-Request an und erhält initial vom Server beispielhaft eine Seite mit folgendem Aufbau:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
...

<title>SPA</title>
<link rel="stylesheet" type="text/css" ↵
href="src/app /index.min.css"/>
</head>
<body>

<nav>
<ul>
<li>Home</li>
...
</ul>
</nav>

<main id="app"></main>

<script src="src/app/bundle.min.js"></script>
</body>
</html>

Neben statischem Markup wie der Navigation gibt es ein ausgezeichnetes DOM-Element mit der ID app (im weiteren "Einstiegspunkt" genannt). Dort hinein rendert der Browser später mit JavaScript weitere DOM-Elemente. Entwickler fassen sämtliches JavaScript, das zur Applikation gehört, in einer Datei zusammen – dem sogenannten Bundle. Es stellt den JavaScript-seitigen Einstiegspunkt dar.

Initial besteht jede SPA aus einem Haupteinstiegspunkt, vergleichbar mit einer Hauptfunktion, in der die Ausführung sämtlicher JavaScript-Logik beginnt. Durch die Integration eines clientseitigen Routers entstehen weitere Einstiegspunkte und somit Teilbereiche innerhalb der Applikation. Diese sind als JavaScript-Funktionen abstrahiert. Vor der Aufteilung ist es hilfreich, sich einen Überblick über die Fachlichkeit der Applikation zu verschaffen. Eine Gliederung auf deren Grundlage hat sich bewährt.

Jeder Teilbereich arbeitet dabei unabhängig und umfasst eigenes Markup und JavaScript. Neben der besseren Übersichtlichkeit verschafft einem ein solches Vorgehen die Flexibilität, Technologien für jeden Teilbereich frei zu wählen und zu kombinieren. Das ist insbesondere deshalb wichtig, weil die Anforderungen pro Bereich der Applikation stark variieren können.

Die Funktionsweise eines Routers lässt sich auf eine Zuordnung von Client-URLs zu Einstiegspunkten zurückführen. Die Zuordnungen sind als Routen bekannt, die sich über einen sogenannten Router steuern lassen. Letzterer lauscht auf Veränderungen der URLs und leitet die Anfrage zum entsprechenden Einstiegspunkt weiter. Bei Single-Page Apps findet während der Navigation auf der Seite kein Roundtrip zum Server statt. Realisieren lässt sich das unter anderem durch den Einsatz des Hash-Zeichens (#), der klassischen HTML-Sprungmarke. Sie ist Bestandteil der URL.

Eine beispielhafte Implementierung eines clientseitigen Routers in ECMAScript 2015 sieht wie folgt aus:

const createRouter = domEntryPoint => {
const routes = new Map();

const addRoute = (hashUrl, routeHandler) => {
routes.set(hashUrl, routeHandler);
}

const navigateToHashUrl = hashUrl => {
location.hash = hashUrl;
}

const handleRouting = () => {
const defaultRouteIdentifier = '*';
const currentHash = location.hash.slice(1);
const routeHandler = routes.has(currentHash) ? ↵
routes.get(currentHash) : routes.get(defaultRouteIdentifier);

if (routeHandler) {
routeHandler(domEntryPoint);
}
};

if (window) {
window.addEventListener('hashchange', handleRouting);
window.addEventListener('load', handleRouting);
}

return { addRoute, navigateToHashUrl };
};

Die Funktion createRouter erzeugt ein neues Router-Objekt mit den Methoden addRoute und navigateToHashUrl. Mit addRoute lassen sich dem Router neue Routen, bestehend aus URL-Hash und der routeHandler-Funktion, hinzufügen. Die Methode navigateToHashUrl ermöglicht den dynamischen Wechsel von Routen im Code unter Angabe des URL-Hashes.

Kern des Routers ist die Funktion handleRouting. Sie wird beim initialen Laden der Seite (load-Event) und bei Änderung des URL-Hashes (hashchange-Event) aufgerufen. Innerhalb von handleRouting ermittelt die Implementierung, ob für den aktuellen Hash eine Zuordnung zu einem Einstiegspunkt (routeHandler) existiert. Falls ja, wird der entsprechende Route-Handler aufgerufen. Als Fallback dient die mit dem Stern (*) gekennzeichnete Default-Route, mit der sich zudem beispielsweise 404-Seiten implementieren lassen.

Die Nutzung des Routers ist im Folgenden veranschaulicht:

const domEntryPoint = document.getElementById('app');
const router = createRouter(domEntryPoint);

router.addRoute('home', domEntryPoint => {
domEntryPoint.textContent = 'Home Route';
});

router.addRoute('overview', domEntryPoint => {
domEntryPoint.textContent = 'Overview Route';
});

router.addRoute('*', domEntryPoint => {
domEntryPoint.textContent = 'Default Route';
});

Beim Erzeugen des Routers wählt die Implementierung den DOM-Einstiegspunkt des initialen Markups und übergibt sie dem Router. Somit lässt sich von außen definieren, wo Inhalte pro Route dynamisch hineinzurendern sind. Jede Route erhält eine Referenz auf das festgelegte DOM-Element. Via domEntryPoint.textContent = '...' wird beispielhaft die aktive Route in Form eines Textes auf der Oberfläche dargestellt.