6. Dynamische Webseiten

In diesem Kapitel geht es kurz und übersichtlich um den Ablauf beim Abrufen dynamischer Webseiten. Es werden auch einige Aspekte zur Optimierung betrachtet. Es ist hilfreich, wenn du dies bei der Erstellung von Webseiten nie aus den Augen zu verlierst.

6.1 Dynamische Webseiten entstehen

Unter dynamischen Webseiten werden Seiten verstanden, deren endgültige, an den Server gesendete Form erst im Augenblick des Abrufes entsteht. So können Daten interaktiv in die Seiten eingebaut werden. Der Vorteil besteht vor allem in der Möglichkeit, auf Nutzereingaben reagieren zu können. Formulare lassen sich sofort auswerten und schon die nächste Seite kann den Inhalt wiedergeben oder Reaktionen darauf zeigen. Die Anwendungsmöglichkeiten sind fast unbegrenzt. Ob und in welchem Umfang außerdem Datenbanken zum Einsatz kommen, hängt von der Zielstellung ab. Dynamische Webseiten an sich benötigen keine Datenbank. Du solltest dich vor allem als Anfänger nicht dem Zwang unterziehen, gleich jedes Problem mit der Hilfe einer Datenbank zu lösen, auch wenn Profis dies bevorzugen werden. Im Buch werden viele Beispiele gezeigt, die mit einfachsten Mitteln beeindruckende Effekte erzielen – ganz ohne Datenbank. Die Entstehung einer dynamischen Website wird in der Abbildung unten erläutert. Dieser Ablauf sollte unbedingt verstanden werden, denn alle anderen, teilweise komplexeren Vorgänge in der Programmierung bauen darauf auf.

Wenn der Benutzer eine Adresse im Browser eintippt, läuft ein recht komplexer Vorgang ab:
1. Der Browser sucht einen Nameserver, um die IP-Adresse zum URL zu ermitteln 2. Der Nameserver konsultiert gegebenenfalls weitere Server, um die IP-Adresse zu beschaffen 3. Der Browser erhält eine IP-Adresse des Servers. Wenn das Protokoll HTTP verwendet wird, ist damit auch die Portadresse festgelegt (Port 80). IP-Adresse und Port bilden eine sogenannte Socket. 4. Der Browser hat eine IP-Adresse vom Provider erhalten und einen Port für die Verbindung gebildet. Damit steht auch ein Socket zur Verfügung. Zwischen beiden Endpunkten kann nun IP-Verkehr stattfinden. 5. Der Browser sendet über diese Verbindung die Anforderung der Seite. Die erfolgt mit dem Protokoll HTTP, die entsprechende Methode lautet GET, der Vorgang wird “Request” oder “Anforderung” genannt. 6. Der Server empfängt die Anforderung und sucht die Datei. Wird diese gefunden, liefert er sie aus. Dieser Vorgang wird “Response” genannt, in diesem Text auch “Antwort”. Wird die Datei nicht gefunden, erzeugt der Server einen Fehler. Für nicht vorhandene Dateien definiert HTTP die Fehlernummer 404. 7. Der Browser empfängt Daten oder eine Fehlermeldung und zeigt diese an.

Zuerst fordert also der Nutzer mit seinem Browser ein Programm an. Der gesamte Vorgang ist letztlich clientgesteuert. Auf dem Webserver wird Anfrage angenommen und HTML-Code oder auch statischer Inhalt erzeugt. Die fertige Seite wird dem Webserver zurückgegeben, der sie dann an den Browser sendet. Damit ist der Vorgang beendet. Beide Seiten “vergessen” alles, was beim Ablauf verwendet wurde. Mit der Anforderung des nächsten Objekts wird der gesamte Ablauf wiederholt. Die Vorgänge der Namensauflösung und Adressbeschaffung laufen völlig transparent ab und sind bei der Programmierung kaum zu berücksichtigen. Der eigentliche Zusammenbau der Seiten ist der interessante Teil. Dies passiert in der Darstellung der Schrittfolge im Schritt 6. Diesen Punkt gilt es also genauer zu untersuchen.

Für alle Probleme liefern diverse Programmierumgebungen interessante und hilfreiche Lösungen. Die Programmierung ist deshalb vergleichsweise einfach. Das ändert aber nichts am Prinzip oder der zugrundeliegenden Technik. Ohne das Ping-Pong-Spiel zwischen Browser und Webserver funktioniert nichts. Manchmal ist es nicht immer sichtbar, dass dieser Prozess tatsächlich abläuft, aber er wird dennoch ausnahmslos ausgeführt. Klar sollte sein, dass in der Webseite, wenn sie an den Browser gesendet wurde, nur noch HTML-Code steht. In diesem mag es weitere Ressourcen geben, die dann aber immer separat angefordert werden. Moderne Packer verpacken viele solcher Aufrufe ein eine JavaScript-Datei, sodass weniger Verkehr entsteht und damit mehr Nutzlast transportiert wird. Dies macht den Ablauf weniger transparent, aber

6.2 Vorgehensweise

Wie entsteht aber nun auf einem lokalen Computer – beispielsweise deinem Laptop – ein solche Webanwendung?

Wenn du jetzt anfangen willst, Web-Anwendungen zu Programmieren, solltest du folgendermaßen vorgehen:

  1. Installiere NodeJS, einen Git-Client und einen Editor
  2. Legen einen Ordner an, wo du deine Projekte organisierst
  3. Legen einen Projektordner an – Projekte bestehen nur aus Dateien aller Art – und installiere alle Hilfspakete, die du brauchst
  4. Bestimme, wie du deine Anwendung bereitstellen möchtest
  5. Bestimme, wie du deine Anwendung gestalten möchtest
  6. Beschaffe eine Vorlage (boilerplate) für dein Projekt
  7. Installieren die Vorlage oder die Vorlagen
  8. Programmiere die Funktionen, die noch benötigt werden
  9. Veröffentliche dein Projekt
  10. Teile Nutzern die Adresse mit, damit die Anwendung getestet werden kann
  11. Beseitige Fehler (Debugging genannt)
  12. Setze mit Punkt 9 fort, bis alles perfekt ist

Soweit, sogut. Aber wie sieht dies konkret aus?

6.2.1 Herunterladen

Vor der Installation sin ein paar Dinge zu beschaffen. Alle Programme in diesem Buch sind Open Source und für die private Nutzung kostenfrei.

Viele Aktionen laufen auf der Kommandozeile (Terminal) ab, nicht grafisch. Versuche mit dieser Umgebung so vertraut wie möglich zu werden. Ohne Kommandozeile wirst du als Entwickler scheitern.

6.2.2 Installation und Konfiguration

Dieser Abschnitt zeigt die grundlegende Konfiguration und den Aufbau einer ersten Node-Umgebung. Das schließt die Nutzung des Paketmanagers mit ein.

Jede Node-Applikation enthält eine Datei mit dem Namen package.json. Damit wird das Projekt konfiguriert. Die Dateierweiterung zeigt an, dass es sich um ein Objekt im JSON-Stil handelt. JSON steht für JavaScript Object Notation und kann von JavaScript besonders einfach verarbeitet werden.

Hier ist ein Beispiel, wie eine solche Datei aussehen kann:

 1 {
 2   "name": "buch-musterprojekt",
 3   "version": "1.0.0",
 4   "description": "Dies ist ein Projekt mit Buchbeispielen.",
 5   "main": "server.js",
 6   "repository": {
 7     "type": "git",
 8     "url": "https://github.com/joergisageek/nodejs-samples"
 9   },
10   "dependencies": {
11     "express": "latest",
12     "mongoose": "latest"
13   },
14   "author": "Jörg Krause",
15   "license": "MIT",
16   "homepage": "http://www.joergkrause.de"
17 }

Achte auf die untergeordneten Objekte, wie dependencies oder repository. Links steht jeweils der Name der Eigenschaft, rechts die Daten. Diese können wiederum Objekte sein. Das geht solange, bis skalare Typen benutzt werden, wie Zeichenkette oder Zahl.

Tatsächlich wird hier nicht alles benötigt. Die einfachste Datei könnte auch so aussehen:

1 {
2   "name": "buch-musterprojekt",
3   "main": "server.js"
4 }

Damit hat das Projekt einen Namen und es hat eine Startdatei – der Einsprungpunkt für den JavaScript-Interpreter. Bei server.js beginnt also die Abarbeitung des Projekts.

6.2.3 Initialisieren der Node-Applikation

Der folgende Abschnitt zeigt zuerst, wie du den Aufbau der Applikation auf der Kommandozeile eines Linux-Systems vornimmst. In den Testbeispielen und zum Anfertigen der Bildschirmfotos wurde Ubuntu benutzt. Es sollte aber jedes andere *nix-System einen vergleichbaren Ablauf erfordern.

6.2.3.1 Vorgehensweise

Dieser Abschnitt setzt voraus, dass du Visual Studio Code installiert hast. Dieser Editor ist sehr leistungsfähig und läuft auf Windows, Linux und MacOS gleichermaßen. Die meisten Funktionen für Node und die Zugriffe auf die Repositories sind bereits fertig vorhanden.

Wie bereits beschrieben startet Node die Applikation über die Anweisungen in der Datei package.json. Damit etwas passiert, sind weitere Schritte notwendig. Das mag lästig erscheinen, aber man macht das ja nur einmal.

6.2.3.2 Eine Node-Applikation starten

Grundsätzlich erfolgt der Start durch Aufruf der ausführbaren Datei node (Linux) bzw. node.exe (Windows). Das Skript läuft durch und endet sofort wieder. Das Programm ist beendet. Soll es dauerhaft laufen, muss dies im Startskript entsprechend programmiert werden. Sollte das Skript laufen und du möchtest es auf der Kommandzeile beenden, nutze die Tastenkombination Ctrl-C.

In npm werden Skripte über npm run <skriptname> gestartet. Es gibt einige wichtige Namen, für die Vorbelegungen vorprogrammiert sind, beispielsweise:

npm start

Dies startet ein Skript mit dem Namen “start”:

1 {
2   "name": "buch-musterprojekt",
3   "version": "1.0.0",
4   "scripts": {
5     "start": "http-server -p 3000 -o index.html"
6   }
7   ...
8 }

Hier wird ein Web-Server gestartet und die Startseite aufgerufen. Der Web-Server muss freilich auch erst beschafft werden. Dazu folgt weiter hinten mehr.

6.2.3.3 Automatischer Neustart

Bei Änderungen sollen diese möglichst einfach überprüft werden. Damit muss das Programm zuerst gestoppt und dann wieder gestart werden – ein ausgesprochen lästiger Vorgang. Das lässt sich jedoch automatisieren, indem Änderungen an einer Datei oder an Dateien in einem Ordner überwacht werden.

Das npm-Paket nodemon liefert diese Funktion. Installiere dies zuerst global:

npm install -g nodemon

Starte dann nicht mit node sondern mit nodemon:

nodemon server.js

node server.js

Alternativ kann npm im aktuellen Ordner benutzt werden:

npm start

Da zu diesem Zeitpunkt server.js nicht existiert, entsteht erstmal eine Fehlermeldung.

6.2.4 Die erste Applikation

Die erste Applikation sollte besonders einfach sein. Die einfachste Version einer package.json-Datei sieht folgendermaßen aus:

1 {
2  "name": "buch-beispiel",
3  "main": "server.js"
4 }

Da diese Konfiguration auf server.js verweist, wird dieses Skript als nächstes erstellt. Damit du siehst, dass es funktioniert, soll es nur Ausgaben mittels console.log erzeugen.

1 console.log('Unsere erste node-Applikation');

Starte die Applikation wie zuvor beschrieben.

6.2.5 Pakete

Pakete erweitern die Funktionalität einer Applikation. Mit Node werden ja nicht nur Web-Applikationen erstellt, sondern auch betriebssystemunabhängige Programme und damit serverseitige Funktionen. Auch für ein einfaches Projekt werden zusätzliche Pakete benötigt – Node selbst ist sehr schlank und modular. Da Node fest mit der Paketverwaltung npm verbunden ist, werden beide Programme zur Nutzung und Verwaltung benutzt.

6.2.5.1 Pakete installieren

In der Konfigurationsdatei package.json werden neben der Applikation selbst auch Abhängigkeiten von weiteren Paketen definiert. Du kannst die Pakete entweder manuell in der Datei eintragen oder dies dem Installationsprozess überlassen.

Hier ein Beispiel, in dem das Paket “Express” mit der Version “4.8.6” als zusätzliche Abhängigkeit definiert wird:

1 {
2   "name": "buch-beispiele",
3   "main": "server.js",
4   "dependencies": {
5     "express": "~4.8.6"
6   }
7 }

Die Versionsnummer wurde hier mit einer Tilde ~ eingeleitet. Dieses Verfahren – die Tilde ist nur eine von viele Möglichkeiten – dient dazu Versionen mit semantischen Informationen zu stärken. Pakete werden schnell weiterentwickelt und bei vielen Abhängigkeiten kann es schwierig sein, sowohl aktuell als auch funktionssicher zu bleiben. Die Tilde sorgt dafür, dass die aktuellste Version im untergeordneten Zyklus benutzt wird. Die Version der dritten Stufe (Patch-Level) darf sich also ändern, die der zweiten (Unterversion) nicht. Erscheint ein Paket mit der Version 4.8.7 oder 4.8.9, so wird dieses benutzt. Erscheint dagegen 4.9.0, so wird es nicht benutzt – der ungetestete Umstieg auf ein solches Release wäre zu riskant.

Eine weitere Methode ist die Installation von Paketen über die Kommandozeile – konkret das Kommandozeilenwerkzeug (oder Command Line Interface, cli) – npm. Meist ist dies schneller und einfacher. Du musst nur entscheiden, ob das Paket nur lokal für eine einzige Applikation oder global für alle künftigen Projekte bereitgestellt wird.

Das Kommando lautet:

npm install <PaketName> --save

Führe das Kommando im Ordner der Applikation aus und gib die Option --save an, dann wird der Eintrag in der Datei package.json automatisch erscheinen. Das Paket selbst (also die Dateien, aus denen es besteht), werden in einem Ordner mit dem Namen node_modules abgelegt.

Nun kann es vorkommen, dass du Pakete in der Datei package.json hast, die noch nicht installiert sind. Der Abruf vom Repository muss erst noch erfolgen. Dazu reicht es aus, in dem Ordner, in dem die Datei package.json liegt, folgendes aufzurufen:

npm install

Abhängigkeiten von weiteren Paketen löst das Kommando selbst auf.

Wenn mehrere Pakete installiert werden sollen, dann können diese in einem Kommando angegeben werden (hier: express, mongoose und passport):

npm install express mongoose passport --save

Die komplette Installation einer Umgebung zum Entwickeln in Node benötigt also nur wenige Kommandos:

  1. npm init initialisiert eine Standardumgebung
  2. package.json konfiguriert diese Umgebung
  3. npm install lädt die benötigten Pakete

Es gibt auch Pakete, die dir als Entwickler helfen, aber nicht Teil der laufenden Umgebung sind. Diese werden im selben Baum verwaltet, in der package.json jedoch anders abgelegt:

Das Kommando lautet:

npm install <PaketName> --save-dev

Dies dient vor allem Dokumentationszwecken.

6.2.6 Eine Serverapplikation erstellen

Node ist eine Serverapplikation. Diese muss gestartet werden, damit Anfragen bearbeitet werden können und Aktionen ausgeführt werden. Während mit Node sehr viel programmiert werden kann – bis hin zu Desktop-Applikationen – ist die Standardanwendung eine Webapplikation. Es gibt deshalb eine Bibliothek, die grundlegende Aufgaben einer Webapplikation übernimmt – Express. Die meisten Beispiele, die du im Web und auf Plattformen wie Stackoverflow findest, nutzen Express.

Der erste Schritt in Node sollte jedoch noch ohne Express erfolgen, um das einfachst mögliche Beispiel zu sehen. Dieser Einführungstext emuliert bewusst einige Funktionen von Express, um die dort stark gekapselte Funktionalität verständlich zu machen.

6.2.6.1 Der einfachste Server

Grundlage der Applikation sind drei Dateien:

package.json wurde bereits betrachtet – dies konfiguriert die Applikation. server.js ist der aktive Einsprungpunkt – dort startet das Skript. index.html ist eine statische HTML-Seite, die hier beispielhaft ausgeliefert wird.

Datei: package.json
1 {
2  "name": "http-server",
3  "main": "server.js"
4 }
Datei: index.html
 1 <!DOCTYPE html>
 2 <html lang="en">
 3 <head>
 4 <meta charset="UTF-8">
 5 <title>Unsere erste Seite</title>
 6 <style>
 7 body {
 8    text-align:center;
 9    background:#EFEFEF;
10    padding-top:50px;
11 }
12 </style>
13 </head>
14  <body>
15 
16  <h1>Hallo Node!</h1>
17 
18  </body>
19  </html>

Die Datei server.js liefert den aktiven Teil:

Datei: server.js
 1 var http = require('http');
 2 var fs = require('fs');
 3 var port = process.env.port || 1337;
 4 
 5 http.createServer(function (req, res) {
 6   console.log("Anforderung auf Port 1337")
 7   res.writeHead(200, {
 8     'Content-Type': 'text/html', 
 9     'Access-Control-Allow-Origin': '*'
10   });
11   var read = fs.createReadStream(__dirname + '/index.html');
12   read.pipe(res);
13 }).listen(port);

Hier werden zuerst zwei Bausteine aus Node benutzt: “http” und “fs”. Das Modul “http” dient dazu, die HTTP-Kommunikation zu programmieren. Mit “fs” (File System) wird dagegen der Zugriff auf das Dateisystem möglich. Damit ist alles vorhanden, was dieses Programm benötigt – die Datei index.html kann gelesen und gesendet werden.

Starte das Projekt nun wie zuvor beschrieben. Wenn nun mit Hilfe eines Browsers ein Abruf der vereinbarten Adresse http://localhost:1337 erfolgt, erscheint die Beispielseite und auf der Konsole die Ausgabe “Anforderung auf Port 1337”.

6.2.7 Dynamisches HTML

Ohne ein Template-System muss viel HTML manuell erstellt werden. Manchmal reicht es, aber das folgende Beispiel zeigt auch, warum sich Template-Engines wie Pug so großer Beliebtheit erfreuen.

Die folgende Erweiterung geht davon aus, dass ein Ordner mit dem Namen files existiert. Die Funktion show der Geschäftslogik wird benutzt, um alle Dateien in diesem Ordner anzuzeigen.

Ausschnitt aus handlers.js (asynchron)
 1 var fs = require('fs');
 2 
 3 function show(response) {
 4   fs.readdir('files', function (err, list) {
 5     response.writeHead(200, { "Content-Type": "text/html" });
 6     var html = '<html><head></head>' +
 7                '<body><h1>Dateimanager</h1>';
 8     if (list.length) {
 9       html += "<ul>";
10       for (i = 0; i < list.length; i++) {
11         html += '<li><a href="/show?fn=' + 
12                  list[i] + '">' + 
13                  list[i] + '</a></li>';
14       }
15       html += "</ul>";
16     } else {
17       html += '<h2>Keine Dateien gefunden</h2>';
18     }
19     html += '</body></html>';
20     response.write(html);
21     response.end();
22   });
23   return true; 
24 }

Hier wird der Ordner mit fs.readdir gelesen und eine Liste von Hyperlinks erstellt; für jede Datei einer. Das HTML wird – mangels Template-Engine – hier einfach im Code zusammengebaut (Zeilen 5, 8 und 10 usw.). Nun müssen die Dateien noch in den Ordner gelangen.

6.2.8 HTML-Dateien senden

Ändere zuerst die HTML-Datei, die an den Browser gesendet werden soll, wie nachfolgend gezeigt:

Datei: views/home.html
 1 <html>
 2   <head>
 3     <meta http-equiv="Content-Type" 
 4           content="text/html; charset=UTF-8" />
 5   </head>
 6   <body>
 7     <h1>Dateimanager</h1>
 8     <a href="/show">Zeige alle Dateien</a>
 9     
10     <form action="/upload" method="post">
11      <input type="file" />
12      <input type="submit" value="Datei hochladen" />
13     </form>
14   </body>
15 </html>

Die Logik ist jetzt bereits in der Lage, eine HTML-Seite von der Festplatte zu laden und an den Browser zu senden. Sie kann außerdem alle Dateien anzeigen.

Als Kodierung (encoding) wurde hier UTF-8 gewählt.

Im letzten Beispiel fehlt zur Komplettierung noch die Funktion upload. Das Übertragen von Dateien erfolgt zusammen mit anderen Formulardaten mit Hilfe der HTTP-Methode POST. Das Absenden erledigt der Browser, wenn ein Formular benutzt wird. Der nächste Schritt besteht zunächst darin, die Methoden zu erkennen und zu beschränken.

6.2.9 Beschränkung der HTTP-Methoden

Der bereits gezeigte Code funktioniert, allerdings reagiert Node auf alle HTTP-Methoden. Das ist in Praxis kritisch, weil unsinnige Wege in die Applikation geöffnet werden. Die Beschränkung besteht also darin, nur auf GET bzw. auf POST zu reagieren.

POST wird nur benötigt, um Daten vom Browser zum Server zu transportieren. Der Server empfängt also alle anderen Anfragen nur mit GET.

Datei: handlers.js
 1 var fs = require('fs');
 2 
 3 function home(request, response) {
 4   if (request.method !== 'GET') {
 5     response.writeHead("405");
 6     response.end();
 7   }
 8   fs.readFile('views/home.html', function (err, data) {
 9     response.writeHead(200, { "Content-Type": "text/html" });
10     response.write(data);
11     response.end();
12   });
13   return true; 
14 }
15 function show(request, response) {
16   if (request.method !== 'GET') {
17     response.writeHead("405");
18     response.end();
19   }
20   fs.readdir('files', function (err, list) {
21     response.writeHead(200, { "Content-Type": "text/html" });
22     var html = '<html><head></head>' +
23                '<body><h1>Dateimanager</h1>';
24     if (list.length) {
25       html += "<ul>";
26       for (i = 0; i < list.length; i++) {
27         html += '<li><a href="/show?fn=' + list[i] + '">' + list[i] \
28 + 
29                 '</a></li>';
30       }
31       html += "</ul>";
32     } else {
33       html += '<h2>Keine Dateien gefunden</h2>';
34     }
35     html += '</body></html>';
36     response.write(html);
37     response.end();
38   });
39   return true; 
40 }
41 function upload(request, response) {
42   if (request.method !== 'POST') {
43     response.writeHead("405");
44     response.end();
45   }
46   return true;
47 }
48 exports.home = home;
49 exports.show = show;
50 exports.upload = upload;

Da die angeforderte Methode in der Anforderung request steht, muss dieser Parameter auch mit übergeben werden. In der Datei start.js sieht Zeile 7 nun wie folgt aus:

var content = route(pathname, handler, request, response);

In der Datei router.js sieht das nun so aus:

Datei: router.js
 1 function route(pathname, handler, request, response) {
 2   console.log("Anforderung für " + pathname);
 3   if (typeof handler[pathname] === 'function') {
 4     return handler[pathname](request, response);
 5   } else {
 6     console.log("Keine Methode gefunden für " + pathname);
 7     return null;
 8   }
 9 }
10 exports.route = route;

6.2.10 Umgang mit Formulardaten

Auf der untersten Ebene werden die Formulardaten als simple Bytefolge weitergereicht. Da hier noch keine hilfreichen Bibliotheken im Einsatz sind, muss die Verarbeitung selbst erfolgen. Es ist Sache des Servers, diese Daten aufzubereiten. Bevor die Methode upload aufgerufen wird, sollten die Daten bereits vorliegen.

Das request-Objekt stellt einige Ereignisse bereit, um auf Daten reagieren zu können. Die Übergabe von request erfolgt bereits im vorhergehenden Schritt, sodass nur wenige Änderungen notwendig sind. Nutzbar sind hier die Ereignisse data beim Eintreffen von Daten und end, wenn keine Daten mehr vorliegen.

1 request.addListener("data", function(chunk) { 
2   // Daten empfangen
3 }); 
4 request.addListener("end", function() { 
5   // Keine Daten mehr
6 });

Das Ereignis data wird mehrfach aufgerufen. Du musst hier die Daten zusammensammeln und dann komplett an die entsprechende Methode übergeben. In handlers.js wird der Parameter postData eingeführt, an den – wenn vorhanden – die Daten übergeben werden. Nun muss nur noch die Datei start.js erweitert werden, damit die Daten ausgewertet werden und natürlich router.js, damit das Weiterreichen funktioniert.

Datei: start.js
 1 var http = require("http");
 2 var url = require("url");
 3 
 4 function start(route, handler) {
 5   function onRequest(request, response) {
 6     var pathname = url.parse(request.url).pathname;
 7     var content;
 8     var postData = '';
 9     request.setEncoding("utf8");
10     if (request.method === 'POST') {
11       request.addListener("data", function (chunk) {
12         postData += chunk;
13       });
14       request.addListener("end", function () {
15         content = route(handler, pathname, 
16                         request, response, postData);
17       });
18     } else {
19       content = route(handler, pathname, response);
20     }
21     var content = route(pathname, handler, 
22                         request, response);
23 
24     if (!content) {
25       response.writeHead(404, { 
26         "Content-Type": "text/plain" 
27       });
28       response.write("404 Not found");
29       response.end();
30     }
31   }
32   var port = process.env.port || 1337;
33   http.createServer(onRequest).listen(port);
34   console.log("Server gestartet.");
35 }
36 
37 exports.start = start;

In Zeile 5 wird eine Variable definiert, die die Formulardaten aufnimmt. Ab Zeile 12 folgen die beiden Ereignisbehandlungsmethoden, in denen die Daten gesammelt werden. Folgen keine Daten mehr, so erfolgt in Zeile 16 der Aufruf des Routers und damit der Aufruf der passenden Methode. Werden keine Daten angefordert wie bei GET, so wird die Router-Methode direkt aufgerufen.

Datei: router.js
 1 function route(pathname, handler, 
 2                request, response, postData) {
 3   console.log("Anforderung für " + pathname);
 4   if (typeof handler[pathname] === 'function') {
 5     return handler[pathname](request, response, postData);
 6   } else {
 7     console.log("Keine Methode gefunden für " + pathname);
 8     return null;
 9   }
10 }
11 exports.route = route;

Der Wert in postData wird einfach durchgereicht. Ist er null oder undefined, so wird auch dies von JavaScript weitergereicht. Eine Fehlerbehandlung ist nicht erforderlich an dieser Stelle.

6.2.10.1 Verarbeiten von Formulardaten

Formulardaten werden in HTTP auf verschiedenen Wegen verarbeitet. Der einfachste Fall sind simple Formularfelder. Dann stehen die Daten in Form einer Kette von Schlüssel-/Wertepaaren in der Anforderung:

Name=Gareth+Wylie&Age=24&Formula=a+%2B+b+%3D%3D+13%25%21

Wenn jedoch Dateien hochgeladen werden, sind diese oft binär und müssen entsprechend kodiert werden. Der Empfänger muss nun wissen, wie er aus den kodierten Daten das ursprüngliche Binärformat wieder erstellen soll. Dazu gibt es den MIME-Standard (multipurpose internet mail extensions). Ursprünglich wurde dies entwickelt, um Bilder in E-Mails einzubetten.

Mit MIME sieht die Kodierung einer Datei etwa folgendermaßen aus:

 1 MIME-Version: 1.0
 2 Content-Type: multipart/mixed; boundary=frontier
 3 
 4 This is a message with multiple parts in MIME format.
 5 --frontier
 6 Content-Type: text/plain
 7 
 8 This is the body of the message.
 9 --frontier
10 Content-Type: application/octet-stream
11 Content-Transfer-Encoding: base64
12 
13 PGh0bWw+CiAgPGhlYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0\
14 aGUgYm9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==
15 --frontier--

Beide Darstellungen deuten an, dass die Verarbeitung von Formulardaten nicht trivial ist, zumal die Beispiele nur einen kleinen Teil der Möglichkeiten wiedergeben. Es ist an der Zeit, hier auf eine weitere npm-Bibliothek zurückzugreifen. Ein guter Start ist die Bibliothek formidable.

Installiere zuerst formidable:

npm install formidable@latest --save

Mache es optional auch global verfügbar, um es in anderen Projekten zu benutzen. Nutze die Option -g und rufe den Befehl damit ein zweites Mal auf.

Eine via POST eintreffende Datei kann damit folgendermaßen empfangen werden:

 1 var formidable = require('formidable'),
 2     http = require('http'),
 3     util = require('util');
 4  
 5 http.createServer(function(req, res) {
 6   if (req.url == '/upload' && req.method === 'POST') {
 7     // Parser
 8     var form = new formidable.IncomingForm();
 9  
10     form.parse(req, function(err, fields, files) {
11       res.writeHead(200, {'content-type': 'text/plain'});
12       res.write('Dateien: ');
13       res.end(files.length);
14     });
15  
16     return;
17   }
18  
19   // Formular
20   res.writeHead(200, {'content-type': 'text/html'});
21   res.end(
22     '<form action="/upload" enctype="multipart/form-data" ' + 
23           'method="post">'+
24     '<input type="text" name="title"><br>'+
25     '<input type="file" name="upload" multiple="multiple">'+
26     '<br /><input type="submit" value="Upload">'+
27     '</form>'
28   );
29 }).listen(8080);

Wichtig ist hier die Gestaltung des Formulars. In Zeile 19 steht enctype="multipart/form-data". Mit diesem Attribut wird das Kodieren nach MIME ausgelöst. Nun wird noch ein Eingabeelement benötigt, dass die Datei auf der Festplatte des Benutzers auswählt (Zeile 21). Die Methode parse wird beim Eintreffen dann die Daten untersuchen und bereitstellen (Zeile 10).

Die Verarbeitungsmethode parse gibt zwei Objekte zurück, files und fields. Darin sind die Dateien zu finden und die anderen Felder des Formulars. Die Struktur sieht etwa folgendermaßen aus:

 1 fields: { title: 'Hello World' }
 2 
 3 files: { 
 4   upload: { 
 5     size: 1558, 
 6     path: '/tmp/1c747974a27a6292743669e91f29350b', 
 7     name: 'us-flag.png', 
 8     type: 'image/png', 
 9     lastModifiedDate: Tue, 21 Jun 2011 07:02:41 GMT, 
10     _writeStream: [Object], 
11     length: [Getter], 
12     filename: [Getter], 
13     mime: [Getter] 
14     } 
15   } 
16 }

Interessant ist hier die Angabe path. Dies ist der temporäre Ort, wo die Datei erstmal abgelegt wurde. Von dort kann diese nun – wenn alle anderen Rahmenbedingungen passen – in den Applikationsordner kopiert werden.

6.2.11 Verarbeiten des Querystring

Die Anzeigemethode soll dazu dienen, die Dateien zum Herunterladen anzubieten. Dazu wird ein Parameter übergeben – der Dateiname. Die Übergabe von Daten in HTTP mittels URL erfolgt über den Teil nach dem Fragezeichen, dem Querystring. Für die Verarbeitung dieser Daten gibt es ein Modul in Node:

var querystring = require("querystring")

Eine separate Installation des Moduls via npm ist nicht notwendig. Wegen der herausragenden Bedeutung ist es immer verfügbar. In der Applikation werden dann die Links zu den Dateien dynamisch erzeugt und ins bestehende HTML eingebettet. Die Dateinamen hängen als Parameter an den Links in der Form fn=filename. Der Querystring muss also auf das Feld fn hin untersucht werden.

Der Abruf der Daten sieht dann folgendermaßen aus:

querystring.parse(request.url.querystring).fn

Das Ergebnis ist der Dateiname oder undefined, falls der Parameter nicht gefunden wurde. Die fertige show-Funktion sieht nun wie folgt aus:

Datei: handlers.js
 1 var fs = require("fs");
 2 
 3 function home(response, postData) { 
 4   // Unverändert
 5 } 
 6 function show(response, postData) { 
 7   if (response.Method !== 'GET') {
 8     response.write("405 Method not allowed");
 9   }
10   console.log("Anforderung 'show' aufgerufen."); 
11   
12   response.write();
13   response.end();
14 } 
15 function upload(response, postData) { 
16   // unverändert
17 } 
18 exports.home = home;
19 exports.show = show; 
20 exports.upload = upload;

Der Querystring steckt in request. Dieses Objekt wird bereits weitergeleitet. Es ist allerdings sinnvoll, die Unterscheidung zwischen Daten aus GET und solchen aus POST aufzulösen und nur mit Daten zu arbeiten. Das kann in der vorherigen Schicht außerhalb der Logik erfolgen, sodass alle Funktionen der Geschäftslogik davon profitieren. Die beiden Methoden sind gegenseitig exklusiv, es kann deshalb nie zu Konflikten kommen. Der Server liefert damit entweder die Daten über form.parse oder über querstring.parse. In beiden Fällen handelt es sich um ein JavaScript-Objekt.

6.3 Die erste Applikation

Mit diesem Code kann die Applikation fertiggestellt werden. Die Bausteine sind:

Praktisch ist jede Webapplikation ähnlich aufgebaut – wenn auch ungleich komplexer. Die primitive innere Struktur von Node führt zu enormer Performance und die Eingriffsmöglichkeiten sind fast grenzenlos. Allerdings bist du als Entwickler gut beraten, sich mit den Grundlagen der Protokolle und elementaren Techniken der Informatik auseinanderzusetzen (HTTP, MIME, Kodierung mit UTF-8 usw.).

Hier das fertige Programm, bestehend aus:

Auf dem Zielsystem muss noch der Ordner files so konfiguriert werden, dass der Prozess, unter dem Node ausgeführt wird, dort (und nur dort) Schreibrechte hat, damit das Hochladen der Dateien funktioniert.

Das fertige Programm nutzt noch eine weitere Node-Bibliothek: mime. Sie dient der Ermittlung des richtigen Content-type-Kopffeldes beim Herunterladen der Dateien. Installiere es wie folgt:

npm install mime --save

6.3.0.1 Die Applikation server.js

Die Applikation startet in der Datei server.js. Hier werden die anderen Module eingebunden. Gegenüber den vorherigen Versionen ist die Vereinbarung der Routen nicht nur an den Namen, sondern auch an die passende HTTP-Methode gebunden. Damit ist die einzelne, wiederholte Abfrage der Methode nicht mehr erforderlich.

Datei: server.js
 1 var server = require("./start");
 2 var router = require("./router");
 3 var requestHandlers = require("./handlers");
 4 
 5 var handler = {};
 6 handler[["/", 'GET']] = requestHandlers.home;
 7 handler[["/show", 'GET']] = requestHandlers.show;
 8 handler[["/upload", 'POST']] = requestHandlers.upload;
 9 
10 server.start(router.route, handler);
6.3.0.2 Das Startskript start.js

Der Funktionsstart selbst wird entsprechend erweitert. Zuerst wurde die Ausführungsmethode in die neue Funktion execute verschoben, da sie mehrfach benötigt wird. Die Geschäftslogik kümmert sich wieder selbst um das Senden der Daten. Nur wenn dies misslingt, wird der generische Fehler 400 Bad request gesendet.

Im Skript werden einige Module benutzt. http, url und querystring sind intern in Node verfügbar. formidable wurde zusätzlich via npm installiert. In der Methode onRequest wird der Pfad für das Routing ermittelt und der Querystring extrahiert (Zeile 16). Bei POST erfolgt noch das Auswerten der Formulardaten.

Aus den Daten wird dann das data-Objekt erstellt, sodass Formulardaten, Querystring-Daten und hochgeladene Dateien an die Geschäftslogik übergeben werden können.

Datei: start.js
 1 var http = require("http");
 2 var url = require("url");
 3 var formidable = require("formidable");
 4 var querystring = require("querystring");
 5 
 6 function start(route, handler) {
 7   
 8   function execute(pathname, handler, request, response, data) {
 9     var content = route(pathname, handler, 
10                         request, response, data);
11     if (!content) {
12       response.writeHead(400, { 
13         "Content-Type": "text/plain" 
14       });
15       response.write("400 Bad request");
16       response.end();
17     }
18   }
19   
20   function onRequest(request, response) {
21     var pathname = url.parse(request.url).pathname;
22     var query = url.parse(request.url).query;
23     if (request.method === 'POST') {
24       var form = new formidable.IncomingForm();
25       form.parse(request, function (err, fields, files) {
26         if (err) {
27           console.error(err.message);
28           return;
29         }
30         var data = { fields: fields, files: files };
31         execute(pathname, handler, request, response, data);
32       });
33     }
34     if (request.method === 'GET') {
35       var data = {
36         fields: querystring.parse(query)
37       };
38       execute(pathname, handler, request, response, data);
39     }
40   }
41   var port = process.env.port || 1337;
42   http.createServer(onRequest).listen(port);
43   console.log("Server gestartet.");
44 }
45 
46 exports.start = start;

Am Server selbst wurde hier nichts geändert – dieser Teil entspricht den vorherigen Beispielen.

6.3.0.3 Die Routingfunktionen router.js

Der Router ist weitgehend unverändert. Einzige Anpassung betrifft die Nutzung von Pfad und HTTP-Verb bei der Wahl der auszuführenden Methode durch ein Array: [pathname, method]. Übergeben wird nur die Antwort response, weil die ausführenden Methoden ihre Daten selbst senden sollen, und die ermittelten Daten der Anforderung. So muss die Anforderung selbst nicht mehr weitergereicht werden.

Datei: router.js
 1 function route(pathname, handler, request, response, data) {
 2   console.log("Anforderung für " + pathname);
 3   var method = request.method;
 4   if (typeof handler[[pathname, method]] === 'function') {
 5     return handler[[pathname, method]](response, data);
 6   } else {
 7     console.log("Keine Methode gefunden für " + pathname +
 8                 " und Verb " + method);
 9     return null;
10   }
11 }
12 exports.route = route;
6.3.0.4 Die Geschäftslogik handler.js

Die Geschäftslogik umfasst die drei Funktionen, die “etwas tun”:

Datei: handler.js (home)
 1 var fs = require('fs');
 2 var path = require('path');
 3 var mime = require('mime');
 4 
 5 function home(response, data) {
 6   fs.readFile('views/home.html', function (err, data) {
 7     response.writeHead(200, { "Content-Type": "text/html" });
 8     response.write(data);
 9     response.end();
10   });
11   return true; 
12 }

Hier wird die HTML-Datei asynchron gelesen und dann an den Client geliefert.

In show werden zwei Aktionen ausgeführt. Zum einen wird der Parameter ‘fn’ abgefragt. Ist dort ein Dateiname zu finden, wird die Datei synchron gelesen und zum Herunterladen ausgeliefert. Zum anderen wird, wenn kein Parameter vorliegt, eine weitere HTML-Seite dynamisch erzeugt, die alle Dateien als Links mit Parameter enthält (ab Zeile 13). Die Steuerung des Herunterladens erfolgt über spezielle Kopffelder, die mit response.setHeader erzeugt werden.

Hier wird das Senden mit response.end vorgenommen, was eine Zusammenfassung aus write und end ist. Die Angabe von ‘binary’ ist zwingend erforderlich, sonst nimmt Node an, dass der Inhalt Text ist und versucht die standardmäßig benutzte Kodierung UTF-8 zu erzwingen. Bilder oder andere Binärdateien werden dadurch jedoch zerstört.

Datei: handler.js (show)
 1 function show(response, data) {
 2   // Herunterladen
 3   if (data.fields && data.fields['fn']) {
 4     var name = data.fields['fn'];
 5     var file = path.join(__dirname, '/files', name);
 6     var mimeType = mime.lookup(file);
 7     response.setHeader('Content-disposition', 
 8                        'attachment; filename=' + name);
 9     response.setHeader('Content-type', mimeType);
10     var filedata = fs.readFileSync(file, 'binary');
11     response.end(filedata, 'binary');
12     return true;
13   }
14   // Alle anzeigen
15   fs.readdir('files', function (err, list) {
16     response.writeHead(200, { "Content-Type": "text/html" });
17     var html = '<html><head></head>' + 
18                '<body><h1>Dateimanager</h1>';    
19     if (list.length) {
20       html += "<ul>";
21       for (i = 0; i < list.length; i++) {
22         html += '<li><a href="/show?fn=' + list[i] + '">' + 
23                 list[i] + '</a></li>';
24       }
25       html += "</ul>";
26     } else {
27       html += '<h2>Keine Dateien gefunden</h2>';
28     }
29     html += '</body></html>';
30     response.write(html);
31     response.end();
32   });
33   return true; 
34 }

Der dritte Teil ist die Funktion zum Hochladen. Auch diese basiert auf Parametern – speziell dem Feld ‘fn’ aus dem HTML-Formular. Trickreich ist die Kopierfunktion copyFile, die Streams benutzt und besonders effizient ist. Die Funktion ist asynchron programmiert und informiert den Aufrufer über die Rückruffunktion callback, wenn die Aktion abgeschlossen ist. Die Funktion upload leitet dann auf die Übersichtsseite show weiter, sodass sich der Benutzer über den Erfolg der Aktion informieren kann.

Datei: handler.js (upload)
 1 function upload(response, data) {
 2   // Hochladen
 3   var temp = data.files['fn'].path;
 4   var name = data.files['fn'].name;
 5   copyFile(temp, path.join('./files', name), function (err) {
 6     if (err) {
 7       console.log(err);
 8       return false;
 9     } else {
10       // Dateiliste anzeigen
11       return show(response, data);
12     }
13   });
14   return true;
15 }
16 
17 function copyFile(source, target, callback) {
18     var rd = fs.createReadStream(source);
19     rd.on('error', function (err) { callback(err); });
20     var wr = fs.createWriteStream(target);
21     wr.on('error', function (err) { callback(err); });
22     wr.on('finish', function () { callback(); });
23     rd.pipe(wr);
24 }
25 
26 exports.home = home;
27 exports.show = show;
28 exports.upload = upload;

Die Daten in data.files['fn'] bieten weit mehr als nur Name und Pfad. So können hier Angaben zum Dateityp, der Dateigröße und dem Datum gefunden werden.

6.3.0.5 Vorlage der HTML-Seite home.html

Als letztes soll nochmal die Formularseite vorgestellt werden. Sie dient dazu, zur Seite mit der Liste der Dateien zu verzweigen und enthält das Formular zum Hochladen.

Datei: home.html
 1 <html>
 2 <head>
 3   <meta http-equiv="Content-Type" 
 4         content="text/html; charset=UTF-8" />
 5 </head>
 6 <body>
 7   <h1>Dateimanager</h1>
 8   <a href="/show">Zeige alle Dateien</a>
 9   <hr />
10   <form action="/upload" method="post" 
11         enctype="multipart/form-data">
12     <input type="file" name="fn" />
13     <input type="submit" value="Datei hochladen" />
14   </form>
15 </body>
16 </html>

Achte auf den Namen des Eingabeelements ‘file’ – name='fn'. Dieser Name muss mit dem im Code benutzten Wert ‘fn’ übereinstimmen. Wichtig ist auch das folgende Attribut:

enctype="multipart/form-data"

Dies erlaubt die Kodierung der Dateien für die Übertragung mit HTTP. Wenn du nur Formulardaten verarbeiten möchtest, jedoch keine Dateien, dann lasse das Attribut weg.

6.3.1 Zusammenfassung

Dieser Abschnitt zeigte eine erste, kompakte Einführung in Node. Es wurden keine zusätzlichen Bibliotheken wie Express oder Template-Engines wie Pug eingesetzt. Da Node recht einfach ist, mussten einige Aktionen, die durch das Protokoll HTTP bedingt sind, selbst programmiert werden. Dafür gibt es natürlich viele fertige Lösungen.

6.4 Optimierung

Optimierung ist heute ein großes Thema, denn es gilt:

“Perfomance is a Feature”

Eine langsame Website wird nie als gut wahrgenommen, egal wie schick sie aussieht oder wie viele Funktionen sie hat.

Bei der Optimierung gibt es viele Aspekte zu beachten:

6.4.1 Werkzeuge

Verstehe zuerst, wie das Web funktioniert. Dazu gehört neben den bereits behandelten Protokollen ein grundlegendes Verständnis für Performance, Bandbreite, Anzahl der HTTP-Requests, das Verhalten von HTML und JavaScript im Browser und schlussendlich das Rendering (Darstellung) und CSS-Effekte. Diverse Werkzeuge helfen dabei, die Vorgänge sichtbar zu machen und dies hilft erheblich dem Verständnis.

Folgende Werkzeuge solltest du haben:

Abbildung: Ablauf der Kommunikation auf einem Zeitstrahl
Abbildung: Ablauf der Kommunikation auf einem Zeitstrahl

Fiddler ist ein Web-Debugger und Protokoll-Proxy. Damit kannst du den Verkehr zwischen Client und Server auf der Client-Maschine sehen und auswerten. In der Ansicht für den zeitlichen Ablauf ist zu erkennen, dass die Anfragen nur teilweise parallel ablaufen. Einige Abfragen werden erst dann ausgelöst, wenn der Browser mit der Verarbeitung der Seite begonnen hat.

Die Abbildung zeigt den Ablauf vom enstehen des Requests (Benutzer klickt auf einen Link) bis zum eintreffen der Daten (Benutzer sieht eine neue Seite). Der Vorgang hat einige Teile, die durch die Infrastruktur bedingt sind und sich kaum beeinflussen lassen (DNS, Aufbau der Request-/Repsonse-Pakete). Der Transfer wird durch die Bandbreite bestimmt, hier lässt sich viel machen. Die Verarbeitung ist in der Hand des Entwicklers, hat aber nicht immer den größten Einfluss. So lohnt es sich im Beispiel beispielsweise nicht, mit viel Aufwand 10% beim Datenbankzugriff zu sparen, denn aus 1400 ms würden dann 1350 ms werden. Ein messbarer, für den Benutzer aber kaum spürbarer Effekt. Wird die Datenmenge dagegen halbiert, sind schon 200 ms drin (100 auf jedem Weg) – 1200 ms also. Letztlich ist es oft die Kombination vieler Maßnahmen, die den spürbaren Effekt bringt.

Abbildung: Ablauf der Kommunikation in Fiddler
Abbildung: Ablauf der Kommunikation in Fiddler

Diese Effekte können beim Aufbau der Seite berücksichtigt werden. Je weniger Abhängigkeiten bestehe, je besser wird die zur Verfügung stehende Bandbreite ausgenutzt. Der Zeitablauf (sogenanntes Wasserfall-Modell) in der letzten Abbildung berücksichtigt auch die Fähigkeiten des Browsers beim Verarbeiten des clientseitigen Codes und den daraus resultierenden Requests und deren Zeitverhalten.

6.4.2 Serverseitige Optimierung

Bei der serverseitigen Optimierung geht es um folgende Themen:

6.4.2.1 Pipeline Optimierung

Im Kern geht es hier um das Entladen (Deaktivieren) von unnötigen Modulen. Webserver kommen mit einer ganzen Reihe von Modulen, die alle möglichen Aufgaben erfüllen. Das beginnt mit der Authentifizierung und endet mit einfacher Protokollierung. Es liegt in der Natur der Module, dass diese jede Anfrage behandeln. Geringe Verzögerungen wirken sich dann drastisch aus.

Für ASP.NET und die IIS sieht dies beispielsweise folgendermaßen aus. Zuerst die standardmäßig aktiven Module:

Abbildung: Module im IIS -- nicht optimiert
Abbildung: Module im IIS – nicht optimiert

Möglicherweise wird aber nur ein Teil davon wirklich benötigt:

Abbildung: Module im IIS -- optimiert
Abbildung: Module im IIS – optimiert

Wenn du mit Apache auf Linux arbeitest, nutze das Kommando a2dismod:

$ sudo a2dismod autoindex

Typische Module, die nicht immer benötigt werden, sind folgende:

6.4.2.2 Prozesskonfiguration

Das Ziel hier ist die optimale Nutzung von Ressourcen. Dazu passt du die Prozesskonfiguration an die konkreten Hardwarebedingungen an. Für einen Windows-Server mit IIS umfasst dies folgende Schritte:

Die Einstellungen der Prozesskonfiguration erfolgt in der machine.config.

Abbildung: Einstellungen der Prozesskonfiguration
Abbildung: Einstellungen der Prozesskonfiguration

Im Apache stellst du den vergleichbaren Wert auf einem Linux-System wie folgt ein:

1 <IfModule mpm_worker_module>
2     ServerLimit          40
3     StartServers          2
4     MaxClients          1000
5     MinSpareThreads      25
6     MaxSpareThreads      75 
7     ThreadsPerChild      25
8     MaxRequestsPerChild   0
9 </IfModule>

Andere Server haben vergleichbare Funktionen, die meist gut dokumentiert sind. Suche gezielt danach!

6.4.2.3 CDN (Content Delivery Network)

Ein Content Delivery Network (manchmal Content Distribution Network genannt), ist ein Netz regional verteilter und über das Internet verbundener Server, mit dem Inhalte – insbesondere große Ressourcen wie Skripte, Bilder oder Videos – ausgeliefert werden. Das Ziel eines CDN sind schnellere Antworten auf Requests und weniger Latenz. Für allgemeine Dateien, wie jQuery, Knockout etc. bietet sich Microsoft, Google etc. an. Für eigene Ressourcen gibt es kostenpflichtige Dienste wie Cachefly (einfach, Upload/Distribute) oder EdgeCast (komplex, DNS Caching).

Abbildung: Prinzip eines Content Delivery Network
Abbildung: Prinzip eines Content Delivery Network
6.4.2.4 Minifizierung und Bundling

Bundling und Minifizierung sind zwei Techniken, mit denen die Ladezeit verbessert werden kann. Dies erfolgt im Wesentlichen über das Zusammenfassen von Ressourcen (Bundling) und damit die Vermeidung von Anfragen. Hintergrund ist, dass aufgrund des Overheads des Protokolls HTTP die Auslieferung vieler kleiner Dateien sehr viel mehr Bandbreite erfordert, als die Auslieferung einer großen Datei.

6.4.2.5 Sprites

Das Erstellen von Sprites ist manuell kaum beherrschbar. Es gibt deshalb eine Vielzahl von Werkzeugen für alle Plattformen und Betriebssysteme. Idealerweise richtest du diese Werkzeuge als Teil des Erstellungsvorgangs ein. Wie das aussieht, hängt vom benutzten Entwicklungssystem ab.

Als Sprite oder CSS-Sprite bezeichnet man eine einzelne Grafikdatei, die mehrere Symbole und Bildbausteine enthält. Diese zusammengefassten Grafiken fungieren als Bildlieferant und dienen dazu, die Ladezeit von Webseiten zu minimieren. Die einzelnen Elemente dieser Gesamtgrafik werden mit den CSS-Eigenschaften background-image und background-position ein- beziehungsweise ausgeblendet und platziert.

Hintergrund ist, dass aufgrund des Overheads des Protokolls HTTP die Auslieferung vieler kleiner Bilder viel mehr Bandbreite erfordert, als die Auslieferung eines großen Bildes.

Hier ein Beispiel für CSS, welches mit Sprites arbeitet:

 1 .flags-canada, .flags-mexico, .flags-usa {
 2   background-image: url('../images/flags.png');
 3   background-repeat: no-repeat;
 4 }
 5 
 6 .flags-canada {
 7   height: 128px;
 8   background-position: -5px -5px;
 9 }
10 
11 .flags-usa {
12   height: 135px;
13   background-position: -5px -143px;
14 }
15 
16 .flags-mexico {
17   height: 147px;
18   background-position: -5px -288px;
19 }

Geladen wird nur ein Bild, flags.png, auf dem alle Flaggen sind. Die Klassendefinitionen sind dann lediglich Positionsangaben auf der Bildfläche.

Das Erstellen von Sprites ist manuell kaum beherrschbar. Es gibt deshalb eine Vielzahl von Werkzeugen für alle Plattformen und Betriebssysteme. Unter Node oder auf einem System, das Node installiert hat, nutze beispielsweise Sprity. Dies wird zuerst installiert:

$ npm install sprity -g

Dann fasst du alle Bilder eines Ordners zu einem Sprite zusammen:

$ sprity ./outputfolder/ ./inputfolder/*.png

6.4.3 Allgemeines und Banales

Generell solltest du darauf achten, bestimmte Techniken nicht zu benutzen:

Redirects werden mit dem Statuscode 302 eingeleitet und fordern den Browser auf, eine andere Site oder Ressource abzurufen. Dies erfordert einen weiteren Zugriff. Jedes Redirect führt also mindestens zu einem Satz für den Benutzer sinnlos übertragener Kopfzeilen. Zudem muss der Browser das nicht zwingend tun. Ihre Website wird etwas unzuverlässiger. Redirects werden von einigen Seiten auch missbraucht (Werb-Netzwerke und Suchmaschineoptimierer tun das), weshalb einige Benutzer dem sehr kritisch gegenüberstehen.

Frames sind technisch veraltetet und in HTML 5 nicht mehr enthalten. Die Anwendung zeigt technisch kundigen Benutzern, dass du eine technologisch völlig veraltete und damit auch unsichere Website gebaut hast.

Absolute Pfade erfordern eine Namensauflösung. Diese ist zwar meist schnell, aber dennoch nicht umsonst. Zudem erschweren absolute Pfade die Pflege der Site und erhöhen die Fehlerquote.

Assets sind alle Hilfsdateien deiner Applikation, also CSS, JavaScript und Bilder, soweit sie in eigenen Dateien vorliegen. Verteile dieses Assets auf verschiedene Hosts (sogenanntes “Off Loading”):

Der Hintergrund ist, dass Browser pro Host nur eine bestimmte Anzahl Anfragen parallel aussenden. Hast du drei Hosts, verdreifacht dies die verfügbare Bandbreite, zumindest bis zum Zugangspunkt. Auch lassen sich die Hosts entsprechend optimieren.

Bei statischen Assets solltest du Cookies und Header vermeiden, wenn es geht. Nutze unbedingt Gzip/Deflate zur Komprimierung. Das ist standardmäßig bei praktisch allen System aktiviert. Konfiguriere den Server dann aber so, dass besser Gzip benutzt wird. Das ETag (Entitätsmarke) wird evtl. nicht benötigt – dieses Kopffeld kann man dann entfernen.

6.4.3.1 Komprimierung

Eine der einfachsten Maßnahmen ist das Verpacken aller HTTP-Nachrichten als ZIP oder mit einem ähnlichen Verfahren. Auf Wikipedia wird dies ausführlicher erläutert. Prinzipiell ist dies einfach zu nutzen. Browser beherrschen das Verfahren ohnehin und größere Webserver müssen nur passend konfiguriert werden. Anders sieht es aus, wenn mit NodeJS der Server komplett selbst aufgebaut wird. In solchen Fällen sind entsprechende Bibliotheken nötig, die das erledigen. Dieses gibt es aber reichlich und für jeden Zweck.

Grundlage ist eine Mitteilung des Browsers, der mittels einer Kopfzeile eine gepackte Verbindung anfordert. Der Server muss das jedoch nicht tun, die Nutzung ist immer optional.

1 Accept-Encoding: gzip, deflate

snappy und zlib sind zwei typische Beispiele solcher Bibliotheken. Wird NodeJs zusammen mit Express eingesetzt – viele Beispiele im Text folgen diesem Schema – lohnt die Middleware compression, die sich transparent in den Kommunikationsweg einbettet und Daten packt. In der einfachsten Form sieht das dann folgendermaßen aus:

1 var express     = require('express')
2 var compression = require('compression')
3 
4 var app = express();
5 app.use(compression());

Mehr Details sind dazu aug Github zu finden.

ETag

ETag (für entity tag, etwa Entitätmarke) ist ein im HTTP 1.1 eingeführtes Header-Feld. Es dient zur Bestimmung von Änderungen an der angeforderten Ressource und wird hauptsächlich zum Caching, also der Vermeidung redundanter Datenübertragungen, verwendet. Quelle: Wikipedia

6.4.4 Clientseitige Optimierung

An dieser Stelle soll nur eine kurze Übersicht der Möglichkeiten als Anregung erfolgen. Du findest zu allen Maßnahmen reichlich Beispiele im Internet.

6.4.4.1 Umgang mit Bildern

Wenn moderne Browser möglich sind, benutze Inline-Bilder. Vor allem für dynamische Bilder, seltene, sehr kleine oder sich häufig ändernde Inhalte ist dies vorteilhaft.

1 <img src="data:image/gif;base64, R0lGODlh...
2 	....	und so weiter  .... "> 

Für die Kodierung nach Base64 eignet sich unter anderem:

Du solltest niemals Bilder im Browser skalieren, sondern immer vorher berechnen (auf dem Server) und komprimiert ausliefern.

Nutze Font-Bibliotheken, sogenannte Symbolfonts, wenn möglich. Fontbasierte Symbole vermeiden das Problem, dass einzelne Symbolbildchen zu einer Flut weiterer Anforderungen auf dem Server führen. Stattdessen werden alle Symbole als ein Font geladen – also in einer Datei. Allerdings ist ein Symbol dann wie ein Buchstabe. Er ist in der Größe und Ausdehnung veränderbar, kann aber nur eine Farbe annehmen. Fonts sind zudem meist flach, 3D-Effekte scheiden hier aus. Für schnelle, moderne Webseiten haben sich Fonts jedoch weitgehend etabliert.

Einige Beispiele für Fonts:

Dies ist freilich nur eine kleine Auswahl und soll dazu anregen, vor den ersten Designversuchen die passende Unterstützung zu suchen.

Abbildung: Der freie Symbolfont Octicons
Abbildung: Der freie Symbolfont Octicons

Wenn du JPG-Dateien nutzen willst, entferne zuerst die Junk-Daten. Das sind Meta-Informationen, die Kameras hinzufügen, teilweise aber auch Programme wie Photoshop. In den Bildern stecken dann kilobyteweise Informationen zum Aufnahmort, Datum, Kameradaten usw.

Achte generell auf das passende Bildformat (JPEG, PNG, GIF etc.).

Abbildung: Bildgröße bei Bildern mit Farbverläufen
Abbildung: Bildgröße bei Bildern mit Farbverläufen

Hier gilt: Je mehr Farben desto größer die Farbtabelle und desto geringer die Kompressionsmöglichkeit. Gadienten (Farbverläufe) sind hier besonders schlecht. Texte mit Antialiasing benötigen mehrere Farben, um Ecken abzurunden. Hier muss ein Kompromiss zwischen Qualität und Größe gefunden werden.

6.4.4.2 Umgang mit dem DOM

JavaScript als Sprache ist extrem schnell. Was problematisch ist, ist der Zugriff auf Elemente im Objektbaum der Seite (document object model, DOM). Ein Beispiel aus der Bibliothek jQuery soll dies illustrieren:

1 $('#dialog-window')
2   .width(600)
3   .height(400)
4   .css('position': 'absolute')
5   .css('top', '200px')
6   .css('left', '200px'); 

Hier wird ein Element #dialog-window adressiert und dann wird sechsmal auf dieses Element zugegriffen. Dies ist sehr unglücklich. Besser ist folgender Zugriff, der alle Werte in einem Schritt ändert:

1 $('#dialog-window').css(
2   { 
3    width: '600px', 
4    height: '400px', 
5    position: 'absolute', 
6    top: '200px', 
7    left: '200px' 
8   }); 

Erstellt wird hier ein Batch beim Rendern. Erstelle dynamische DOM-Blöcke separat und füge diese dann dem gesamten Baum in einem Schritt zu. Einzeln führt dies zu vielen Zeichenvorgängen, zusammengefasst wird nur ein Auffrischen des Bildschirms ausgelöst. Neuere Bibliotheken wie React bieten ein solches Verhalten bereits implizit.

Hintergrund ist, dass der Browser bei jeder Änderung am DOM die Oberfläche neu zeichnet. Sequenzen mit viele kleinen Änderungen erfordern eine erhebliche Rechenleistung. Auch wenn diese vermeintlich zur Verfügung steht, heißt es dennoch, dass bei mobilen Geräten viel Strom und damit Akkulaufzeit verbraucht wird.

Mehr zu jQuery und anderen wichtigen Bibliotheken findest du weiter hinten im Text.

6.5 Authentifizierung

Praktisch kommt kein Projekt ohne Authentifizierung aus.

Heute wird dazu häufig OAuth eingesetzt. Warum ein einfaches Schema aus Benutzername und Kennwort nicht ausreicht, lässt sich anhand einer einfachen Sitation zeigen. Ein Anbieter betreibt mehrere Websites, davon einige mit Hilfe eines Partners, der andere Tehnologien einsetzt. Viele Benutzer nutzen diese und andere Sites, um die Leistungen in Anspruch zu nehmen. Würde nun jede Site Benutzername und Kennwort anfordern, müsste sich der Bneutzer mehrfach anmelden und sein Kennwort mehrfach hinterlegen. Der Sitebetreiber müsste überdies auch mehrere Benutzerdatenbanken pflegen. Beiden Seiten entsteht hoher Aufwand.

6.5.1 Federation

Eine Identität ist eine zusammengefasste Identität, die sich über mehrere Systeme erstreckt. Identitätsinformationen werden in verschiedenen Systemen gehalten und genutzt. Derartige Dienste stellen praktisch Anmeldeverfahren bereit und können öffentlich oder private sein. Bekannte öffentliche Provider sind Microsoft, Google, Facebook, Twitter aber auch Github. Es gibt sehr viele weitere. Private Implementierungen sind Identity Server oder Active Directory Federation Services (ADFS).

Das folgende Bild fasst die Abläufe zusammen:

Abbildung: Vollständiger Ablauf einer Anmeldung
Abbildung: Vollständiger Ablauf einer Anmeldung

Für den Benutzer ist es nun einfacher, mit vielen Diensten zu kommunizieren, ohne jedesmal das Kennwort mitteilen zu müssen. Die Dienste sind einfacher, weil sie keine explizite Benutzerverwaltung mehr benötigen. Der Kanal ist sicher, weil der Fedearation-Provider zwar die gemeinsame vertraute Instanz ist, von der eigentlichen Kommunikation (Schritte 11 und 12) aber nichts mitbekommt.

6.5.2 OAuth

OAuth (Open Authorization) ist ein offenes Protokoll, das eine standardisierte, sichere API-Autorisierung für Desktop-, Web- und Mobile-Anwendungen erlaubt. Es entstand 2006. OAuth verwendet Tokens zur Autorisierung eines Zugriffs auf geschützte Ressourcen. Dadurch kann einem Client Zugriff auf geschützte Ressourcen gewährt werden, ohne die Zugangsdaten des Dienstes an den Client weitergeben zu müssen.

In OAuth 2.0 existieren vier Rollen:

6.5.3 Autorisierung

Die Autorisierung ist oft von der Geschäftslogik abhängig.

6.6 Framework-Bausteine

Jede Applikation benötigt weitere Bausteine. An dieser Stelle soll die Nennung bewährter Beispiele genügen, da die entsprechenden Quellen gute Informationen zur Benutzung bereithalten.