7. Dynamische Webseiten mit Node

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.

7.1 Wie 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.

7.2 Vorgehensweise

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

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

  1. Installiere NodeJS und einen Git-Client
  2. Legen einen Ordner an, wo du deine Projekte organisierst
  3. Legen ein Projekt 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
  12. Setze mit Punkt 9 fort, bis alles perfekt ist

Soweit, sogut. Aber wie sieht dies konkret aus?

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.

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.

Vorgehensweise unter Linux

Wie bereits beschrieben startet Node die Applikation über die Anweisungen in der Datei package.json. Damit da nichts falsch läuft, gibt es ein npm-Kommando, dass diese Datei erstellt: npm init. Um mit einer neuen Node-Applikation zu starten, gehe folgendermaßen vor:

  1. Erzeuge einen Ordner: mkdir buch-projekt
  2. Wechsele in diesen Ordner: cd buch-projekt
  3. Initialisiere das Projekt: npm init

Du kannst beim ersten Mal die interaktiv abgefragten Parameter unverändert lassen. Benenne nur die Startdatei um in server.js. Die Node-Applikation ist jetzt startbereit – auch wenn noch nicht viel sinnvolles passiert – und kann gestartet werden.

Vorgehensweise unter Windows

Dieser Abschnitt setzt voraus, dass du Visual Studio 2015 oder 2017 installiert hast. Die meisten Funktionen für Node und die Zugriffe auf die Repositories sind bereits fertig vorhanden.

Ein neues einfaches Node-Projekt wird über die Projektvorlage Blank Node.js Web Application erzeugt.

Abbildung: Node.js Projektvorlage
Abbildung: Node.js Projektvorlage

Wie bereits beschrieben startet Node die Applikation über die Anweisungen in der Datei package.json. Diese Datei ist im neuen Projekt bereits vorhanden. Als Startdatei wird server.js benutzt. Die Node-Applikation ist jetzt bereits startbereit – auch wenn noch nicht viel sinnvolles passiert – und kann gestartet werden. Drücke wie immer einfach F5. Node startet in einer Konsole und der Browser öffnet sich mit der Ausgabe “Hello World”. Diese Ausgabe wurde von der Projektvorlage erzeugt.

Im weiteren Verlauf des Textes wird der Vorgang für Visual Studio nicht jedesmal gezeigt, sondern die Kommandozeilenversion für Linux benutzt. Die Unterschiede sind minimal und in der folgenden Tabelle zusammengefasst.

Tabelle: Unterschiede Linux/Windows
Aktion Linux Windows+VS 2015
Starten npm start F5
Paket installieren npm install pkg Kontextmenü auf Ordner ‘npm’: Install new Packages > Paket suchen > Install Package
Abbildung: npm-Pakete mit Visual Studio 2015
Abbildung: npm-Pakete mit Visual Studio 2015
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 in server.js entsprechend programmiert werden. Sollte das Skript laufen und du möchtest es auf der Kommandzeile beenden, nutze die Tastenkombination Ctrl-C. In der Praxis wirst du unter Linux wie zuvor bereits beschrieben npm zum Start benutzen. Unter Windows mit Visual Studio ist F5 (Debug > Start Debugging) der einfachste Weg, lokal zu starten.

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 ü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.

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.

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.

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 darf sich also ändern, die der zweiten 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

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.

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”.

Dynamisches HTML

Ohne Template-System muss viel HTML manuell erstellt werden. Manchmal reicht es, aber das folgende Beispiel zeigt auch, warum sich Template-Engines wie JADE 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. Nun müssen die Dateien noch in den Ordner gelangen.

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 des HTTP-Verbs POST. Das Absenden erledigt der Browser, wenn ein Formular benutzt wird. Der nächste Schritt besteht zunächst darin, die Verben zu erkennen und zu beschränken.

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;

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. Liegen keine Daten vor, beispielsweise 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.

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 aGUg
15 Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg==
16 --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. Mache es optional auch global verfügbar (Option -g), um es in anderen Projekten zu benutzen:

npm install formidable@latest --save -g

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 sind 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.

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. Auch für die Verarbeitung dieser Daten gibt es ein Modul in Node:

var querystring = require("querystring")

Eine separate Installation des Moduls 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 Methoden der Geschäftslogik davon profitieren. Die beiden Verben 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.

7.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

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 das passende HTTP-Verb 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);
Das Startskript start.js

Der Funktionsstart selbst wird entsprechend erweitert. Zum einen ist 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.

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;
Die Geschäftslogik handler.js

Die Geschäftslogik umfasst die drei Methoden, 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.

Vorlage der HTML-Seite home.html

Als letztes soll nochmal die Formularseite vorgestellt werden. Dies dient dazu, zur Seite mit der Liste der Dateien zu verzweigen und sie 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.

Zusammenfassung

Dieses Kapitel 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.

7.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:

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.

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.

Serverseitige Optimierung

Bei der serverseitigen Optimierung geht es um folgende Themen:

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:

In Testumgebungen spielt beides keine Rolle. Hier wird meist NodeJS benutzt und dann so schlank konfiguriert, dass ohnehin nur das läuft, was unbedingt benötigt wird.

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 die 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 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>
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
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.

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.

Sprites

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.

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 }

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

NodeJS nutzen

npm ist der Node Packet Manager. Damit lassen sich Pakete abrufen und ausführen, die in JavaScript geschrieben sind und Node nutzen. Installiere zuerst Node von der Website https://nodejs.org/. npm ist im Paket enthalten. Öffne dann eine Kommandzeile (Terminal) und führe die Befehle dort aus. Mehr zu Node und JavaScript-Werkzeugen findest du weiter hinten im Buch.

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

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

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 HTTP 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.

Verteile Assets auf verschiedene Hosts (sogenanntes “Off Loading”):

Der Hintergrund ist, dass Browser pro Host nur eine bestimmte Anzahl Anfragen parallel aussenden (zwischen 6 und 13). Hast du drei Hosts, verdreifacht dies die verfügbare Bandbreite, zumindest bis zum Zugangspunkt.

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.

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

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.

Umgang mit Bildern

Wenn moderne Browser möglich sind, benutzen Inline-Bilder. Vor allem für dynamische Bilder, seltene, 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 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 weit etabliert.

Einige Beispiele:

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, entfernen 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.

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](https://de.wikipedia.org/wiki/Antialiasing_(Computergrafik) benötigen auch mehrere Farben, um Ecken abzurunden. Hier muss ein Kompromiss zwischen Qualität und Größe gefunden werden.

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    width: '600px', 
3    height: '400px', 
4    position: 'absolute', 
5    top: '200px', 
6    left: '200px' }); 

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.

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 Buch.

7.5 Authentifizierung

OAuth und passport

7.6 Optimierung

Snappy und zlib

express.static

Gzip-Compression

7.7 Framework-Bausteine

Logging

Debug, Winston, Bunyan

Cookies

cookie-parser

Sitzungen

7.8 Datenbanken mit MongoDb

MongoDb, mongoose