8. Die Serverumgebung

Im Kapitel zu dynamischen Webseiten wurde bereits ein sehr kompaktes Beispiel mit Node.js (kurz Node) gezeigt. Node spielt hier eine herausragende Rolle, um weitgehend unabhängig von Herstellern und Anbietern zu entwickeln. Du musst auch keine weitere Sprache wie Ruby, C#, Java oder Python lernen, um Server zu programmieren. JavaScript alleine reicht aus und mit TypeScript macht es mehr Spaß.

Deshalb an dieser Stelle eine kompakte Einführung in Node.

8.1 Node

Dieses Kapitel zeigt die wichtigstens Module, mit denen elementare Aufgaben in einer Webapplikation erledigt werden können. Es handelt sich dabei um die eigentliche Node-Bibliothek.

8.1.1 Globale Module

Globale Module sind immer vorhanden und müssen nicht vereinbart werden.

8.1.1.1 Timer

Zeitgeber abstrahieren weitgehend die von JavaScript standardmäßig angebotenen Möglichkeiten. Nutze unbedingt die Node-Variante, um später keine Probleme mit anderen parallel laufenden Modulen zu bekommen.

Der Befehl setTimeout vereinbart den Aufruf der Rückruffunktkon nach einem bestimmten Zeitraum in Millisekunden. Optional können Argumente angegeben werden. Die Funktion gibt ein Objekt vom Typ timeoutObject zurück, dass mit clearTimeout() benutzt werden kann.

Syntax: setTimeout(callback, delay[, arg][, ...])

Die Funktion clearTimeout verhindert den Aufruf.

Syntax: clearTimeout(timeoutObject)

Auch die Funktion setInterval entspricht der internen JavaScript-Funktion, läuft aber unter Kontrolle von node ab. Die Rückruffunktion wird wiederholt nach Ablauf des Intervalls aufgerufen. Die Funktion gibt ein Objekt vom Typ intervalObject zurück, dass mit clearInterval() benutzt werden kann.

Syntax: setInterval(callback, delay[, arg][, ...])

Die Funktion clearInterval stoppt den wiederholten Aufruf.

Syntax: clearInterval(intervalObject)

Die Methode unref wird von den Objekten timeoutObject und intervalObject angeboten. Wenn eine node-Applikation endet, und sich noch Zeitgeber in Aktion befinden, wird die Ausführung dennoch fortgesetzt, bis der letzte Zeitgeber abgelaufen ist. Mit unref kann angezeigt werden, dass die Beendigung der Applikation auch die übriggebliebenen Zeitgeber stoppt und nicht weiter ausführt. Der mehrfache Aufruf von unref auf demselben Objekt hat keinen Effekt.

Die Funktion verschiebt den Zeitgeber in die Hauptschleife der Applikation. Zuviele solche Zeitgeber können die Leistung der Hauptschleife beeinflussen. Du solltest unref daher bewusst und nur falls unbedingt notwendig einsetzen.

Ein zuvor mit unref in die Hauptschleife verschobener Zeitgeber kann mit der Funktion ref wieder in seinen regulären Zustand überführt werden. Der mehrfache Aufruf hat keine Effekt.

Die Methode setImmediate ist ein höher priorisierter Zeitgeber, der nach I/O-Ereignissen auslöst und vor setTimeout und vor setInterval aufgerufen wird. Dieser Zeitgeber gibt ein Objekt immediateObject zurück, das mit clearImmediate() benutzt werden kann. Mehrere Rückruffunktionen werden in einer Warteschlange platziert und in der Reihenfolge abgearbeitet, wie sie definiert wurden. Die Ausführung der Warteschlange erfolgt einmal pro Durchlauf der Hauptschleife der Applikation. Ein neu platziertes Objekt wird also erst dann ausgeführt, wenn die Hauptschleife das nächste Mal durchläuft.

Syntax: setImmediate(callback[, arg][, ...])

clearImmediate stoppt die Ausführung des mit immediateObject bezeichneten Zeitgebers.

Syntax: clearImmediate(immediateObject)

8.1.2 Globale Objekte

Globale Objekte sind in allen Modulen aktiv. Sie müssen nicht separat vereinbart werden.

Mit global ist der globale Namensraum gemeint. Eine Variable in JavaScript ist im globalen Namensraum, auch wenn sie mit var definiert wurde, global. In Node ist dies nicht der Fall – der “globale” Namensraum ist immer das aktuelle Modul. Erst durch den expliziten Zugriff auf global wird ein globaler Namensraum möglich.

Das process-Objekt zeigt Informationen zum Prozess an. Mit console diesem Objekt besteht Zugriff auf die Konsole. Das Buffer-Objekt beinhaltet den Umgang mit gepufferten Daten.

Die Funktion requirefordert ein Modul an. Diese Funktion ist nicht wirklich global, sondern wird automatisch in jedem Modul lokal vereinbart, sodass sie wie eine globale Funktion immer verfügbar ist.

Die Methode require.resolve nutzt den Suchmechanismus für Module, lädt aber im Erfolgsfall das Modul nicht, sondern gibt lediglich den Pfad zurück, unter dem des gefunden wurde. Module können lokal oder global installiert sein, sodass der Fundort durchaus variiert. Mit require.cache werden Module in dem Objekt, dass diese Eigenschaft zurückgibt, gecacht. Wenn das Modul aus dem Cache durch Löschen des Schlüssels entfernt wird, wird der nächste Aufruf von require das Modul erneut laden.

__filename ist der Dateiname der aktuell ausgeführten Code-Datei. Der Name enthält den aufgelösten, absoluten Pfad. Dies muss nicht derselben Pfad sein wie er mit den Kommandozeilenwerkzeugen benutzt wird. Wenn der Aufruf in einem Modul erfolgt, ist das Modul die ausgeführte Code-Datei und der Pfad zeigt zum Modul.

Wenn beispielsweise die Datei example.js im Pfad /User/joerg/Apps ausgeführt wird, gibt der folgende Aufruf /User/joerg/Apps/example.js zurück:

console.log(__filename);

__filename ist global nutzbar, wird aber in jedem Modul lokal definiert.

__dirname ist das Verzeichnis, in dem die aktuell ausgeführte Datei ist.

Wenn beispielsweise die Datei example.js im Pfad /User/joerg/Apps ausgeführt wird, gibt der folgende Aufruf /User/joerg/Apps zurück:

console.log(__dirname);

__dirname ist global nutzbar, wird aber in jedem Modul lokal definiert.

module ist eine Referenz zum aktuelle Modul. Die Eigenschaft module.exports wird dazu benutzt, die vom Modul exportierten Funktionen bereitzustellen. Verfügbar gemacht werden diese durch den Aufruf von require().

module ist global nutzbar, wird aber in jedem Modul lokal definiert.

exports ist ein Alias für module.exports und verkürzt lediglich den Schreibaufwand. exports ist global nutzbar, wird aber in jedem Modul lokal definiert.

8.2 HTTP und HTTPS

Mit den http- bzw. https-Modulen werden nahezu alle Fassetten der Protokolle HTTP und HTTPS unterstützt. Die Kommunikation auf dieser Ebene ist sehr elementar. Frameworks wie Express abstrahieren dies und setzen selbst auf http auf. Trotzdem kann es für viele Fälle sinnvoll sein, Protokollaktionen direkt auszuführen.

Node kann mit Streams umgehen – also einem fortlaufenden Strom von Bytes. Dies ist weit effektiver als die gesamten Daten für einen Vorgang im Speicher zu halten (Puffer, buffering). Die http-Module kümmern sich um das Verarbeiten von Daten mit Streams und erleichtern erheblich die Programmierung.

8.2.1 Grundlagen

HTTP besteht aus einer Befehlszeile und Kopffeldern, die den Befehl näher beschreiben. In Node werden die Kopffelder als JSON bereitgestellt. Ein entsprechendes Objekt könnte also folgendermaßen aussehen:

1 { 
2   'content-length': '123',
3   'content-type': 'text/plain',
4   'connection': 'keep-alive',
5   'host': 'mysite.com',
6   'accept': '*/*' 
7 }

Die Schlüssel werden entsprechend der Spezifikation immer in Kleinbuchstaben konvertiert. Die Werte werden dagegen niemals verändert. Das ist bereits der ganze Eingriff von Node an dieser Stelle. Generell ist Node bei diesem Modul sehr einfach. Weder die Kopffelder noch der Inhalt einer Nachricht werden untersucht, bewertet oder intern behandelt.

Kopffelder, die mehrere Werte haben, nutzen das , (Komma) zum Trennen der Werte. Einzige Ausnahme sind die Kopffelder für Cookies, die ein Array akzeptieren. Wenn Felder nur einen Wert erlauben, kontrolliert Node dies und wirft eine Ausnahme.

Eintreffende oder gesendete Kopffelder werden als unbearbeitetes Objekt bereitgestellt. Dies ist ein Array mit fortlaufenden Paaren von Schlüsseln und Werten. Das sieht etwa folgendermaßen aus:

1 [ 'Content-Length', '123456',
2   'content-type', 'text/plain',
3   'CONNECTION', 'keep-alive',
4   'Host', 'mysite.com',
5   'accept', '*/*' ]

Umwandlungs- und Kontrollaktionen finden danach statt, sodass die tatsächlich bereitgestellten oder gesendeten Kopffelder davon abweichen können.

8.2.2 Felder

Dieser Abschnitt beschreibt Felder, die Werte bereitstellen, die auf die interne Konfiguration verweisen.

http.METHODS gibt in Form eines Arrays eine Liste der HTTP-Verben zurück, die unterstützt werden. http.STATUS_CODES ist ein Array mit den Statuscodes, die HTTP kennt, und dem zugeordneten Kurztext. Für 404 ist dies beispielhaft wie folgt definiert:

http.STATUS_CODES[404] === 'Not Found'

8.2.3 Methoden

Die Methoden ermöglichen die entsprechenden Aktionen in Bezug auf die Protokollverarbeitung. http.createServer gibt eine neue Instanz des Http-Servers zurück. Damit können HTTP-Anfragen empfangen und verarbeitet werden. Die Syntax sieht folgendermaßen aus:

http.createServer([requestListener])

Die Rückruffunktion requestListener ist eine Methode, die die empfangenen Daten bekommt.

Mit http.request(options[, callback]) sendet Node eine Anforderung (request) an einen anderen Server. Node ist also in diesem Fall der Client. Node benutzt mehrere Verbindungen, wenn dies möglich ist. Die Methode behandelt dies jedoch intern, sodass Du beim Programmieren darauf keine Rücksicht nehmen musst. Folgende Syntax wird benutzt:

http.request(options [, callback])

Die Optionen können JSON oder eine Zeichenkette sein. Ist es eine Zeichenkette, wird automatisch url.parse() eingesetzt, um die Zeichenkette zu parsen. Die Rückrufmethode liefert ein Objekt mit der Antwort (response).

Die Optionen haben folgende Bedeutung:

Die Methode selbst gibt eine Instanz der Klasse http.ClientRequest zurück. Dies ist ein schreibbarer Stream. Werden für die Anfrage Daten benötigt, beispielsweise weil bei einer POST-Anforderung ein Formular gesendet wird, dann werden diese Daten in diesen Stream geschrieben.

 1 var postData = querystring.stringify({
 2   'msg' : 'Hallo Node!'
 3 });
 4 
 5 var options = {
 6   hostname: 'www.google.com',
 7   port: 80,
 8   path: '/upload',
 9   method: 'POST',
10   headers: {
11     'Content-Type': 'application/x-www-form-urlencoded',
12     'Content-Length': postData.length
13   }
14 };
15 
16 var req = http.request(options, function(res) {
17   console.log('STATUS: ' + res.statusCode);
18   console.log('HEADERS: ' + JSON.stringify(res.headers));
19   res.setEncoding('utf8');
20   res.on('data', function (chunk) {
21     console.log('BODY: ' + chunk);
22   });
23 });
24 
25 req.on('error', function(e) {
26   console.log('problem with request: ' + e.message);
27 });
28 
29 req.write(postData);
30 req.end();

Das eigentliche Schreiben erfolgt mit req.write(postData). Die Benutzung von req.end() ist hier notwendig, weil der Stream sonst nicht geschlossen wird. Nach dem Beenden können keine weiteren Daten geschrieben werden. Das Anforderungsobjekt req kennt ein Ereignis error, auf das du reagieren kannst, um Fehler abzufangen. Fehler können auftreten, wenn einer der Vorgänge beim Senden misslingt (DNS-Auflösung, TCP-Fehler, Fehler beim Parsen der Kopffelder usw.).

Wird das Kopffeld Connection: keep-alive manuell eingefügt, erkennt Node dies und hält die Verbindung offen, bis die nächste Anfrage gesendet wird.

Wird das Kopffeld Content-length gesendet, dann wird die Benutzung von Chunks abgeschaltet. Chunks sind das blockweise Senden von Daten. Die Angabe erfolgt durch das Kopffeld Transfer-Encoding: chunked.

Wird ein Expect-Kopffeld benutzt, dann werden die Kopffelder sofort gesendet. Nach Expect: 100-continue sollte sofort auf das entsprechende Ereignis gelauscht (mit Timeout) werden. RFC2616 Section 8.2.3 gibt dazu mehr Informationen.

Wenn das Kopffeld Authorization angegeben wird, werden die durch die Option auth erzeugten Daten überschrieben.

Mit http.get steht eine verkürzte Variante der Methode request bereit, die eine Anfrage mittels ‘GET’ initiiert. Das bei GET keine Daten gesendet werden, wird req.end() automatisch erzeugt:

http.get(options[, callback])

Ein Beispiel zeigt, wie es geht:

1 http.get("http://www.google.com/index.html", function(res) {
2   console.log("Got response: " + res.statusCode);
3 }).on('error', function(e) {
4   console.log("Got error: " + e.message);
5 });

8.2.4 HTTP-Klassen

Einige Klassen liefern weitere Funktionalität.

8.2.4.1 http.Server

Der HTTP-Server bietet eine Umgebung, die auf Aktionen des Protokolls mittels Ereignisse reagiert. Die Ereignisse sind:

Die Ereignisse werden über die Methode on erreicht:

1 var http = require("http");
2 var server = http.createServer();
3  
4 server.on("request", function (req, res) {
5     res.end("this is the response");
6 });
7  
8 server.listen(3000);
8.2.4.2 Methoden für http.Server

Das Objekt server selbst, das createServer erzeugt, verfügt über einige Methoden, die ebenfalls interessant sind.

Mit server.listen beginnt der Server am angegebenen Port und der entsprechenden Adresse – also dem Socket – zu lauschen. Wird der Hostname nicht angegeben, wird an allen IP-Adressen auf der Maschine gelaucht (nur IPv4). Foldende Varianten gibt es:

server.listen(port[, hostname][, backlog][, callback])

server.listen(path[, callback])

server.listen(handle[, callback])

Der Parameter backlog ist die Länge der Pufferwarteschlange für eintreffende Verbindungen. Wenn ein Verbindungswunsch eintrifft und der Vorgehende sich noch in Verarbeitung befindet, dann nimmt Node diese Anfrage in diese Warteschlange auf. Der Standardwert ist 511 (!sic). Werte bis 1000 sind hier sinnvoll. Zulange Warteschlangen suggerieren Clients, dass eine Verbindung zu erwarten ist, während Node kaum in der Lage ist, diese auch zu verarbeiten.

Wird ein handle benutzt, so ist dies ein Objekt, dass einen Server oder ein Socket beschreibt.

Die Funktion ist asynchron und arbeitet mit der Rückrufmethode callback.

Mit server.close stoppt der Server das Akzeptieren von Verbindungswünschen:

server.close([callback])

Da Verbindungen möglicherweise nicht zur Verfügung stehen, kann mit server.setTimeout ein Wert gesetzt werden, der bestimmt, wielange gewartet wird:

server.setTimeout(msecs, callback)

Der Wert wird in Millisekunden angegeben. Der Standardwert beträgt 2 Minuten. server.timeout gibt den gesetzten Wert wieder.

Mit server.maxHeadersCount wird die Anzahl der Kopffelder begrenzt. Standardmäßig sind dies 1000, mit 0 ist der Wert unbegrenzt.

8.2.4.3 Die Klasse http.ServerResponse

Eine Instanz dieser Klasse wird intern erstellt. Dies ist der Typ, der durch den Parameter response in der Rückruffunktion des Ereignisses request übergeben wird. Dies ist das Antwort-Objekt. Es implementiert einen schreibbaren Stream. Dieser arbeitet mit Ereignissen.

‘close’: function () { }
Zeigt an, dass die Verbindung geschlossen wurde, bevor end in der Lage war, die Daten zu senden. ‘finish’: function () { }
Wird ausgelöst, wenn die Übertragung der Antwort erledigt ist. Für Node ist dies der Moment der Übergabe an das Betriebssystem. Das heißt nicht zwingend, dass die Daten den Computer verlassen haben oder der Client diese Daten empfangen hat.

Auf eine Instanz dieser Klasse sind vielfältige Operationen möglich. response.writeContinue sendet ein HTTP/1.1 100 Continue an den Client um ihn aufzufordern, dass die Daten gesendet werden können. Mit response.writeHead sendet Node den Kopf – Statuscode plus Kopffelder – an den Client. Der Statuscode ist der dreistellige HTTP-Code, beispielsweise 200 oder 404. Die Kopffelder können passend dazu angegeben werden. Folgende Syntax ist anwendbar:

response.writeHead(statusCode[, statusMessage][, headers])

1 var body = 'hello world';
2 response.writeHead(200, {
3   'Content-Length': body.length,
4   'Content-Type': 'text/plain' }
5 );

Diese Methode darf nur einmal aufgerufen werden und dies muss vor response.end() erfolgen.

Alternativ kann mit response.write() und response.end() gearbeitet werden. Wird response.write() benutzt und ist die Antwort noch nicht beendet worden, berechnet Node beim Aufruf von writeHead die kumulierten Kopffelder.

Mit response.setTimeout wird der Timeout-Wert in Millisekunden gesetzt:

response.setTimeout(msecs, callback)

Die Rückruffunktion callback wird aufgerufen, wenn die Zeit abläuft. Wenn keine Angabe erfolgt, werden nach Ablauf der Zeit die entsprechenden Objekte für Socket, Server, Response usw. aufgeräumt. Ist aber eine Rückruffunktion vorhanden, so musst du dies in dieser Funktion selbst erledigen.

Mit response.statusCode legst du fest, welcher Statuscode benutzt wird. Dies ist nicht notwendig, wenn du mit writeHead arbeitest.

response.statusCode = 404;

Die Eigenschaft enthält den tatsächlichen Wert nach dem Senden der Antwort.

Legen mit response.statusMessage dieser Eigenschaft fest, welcher Statuscode benutzt wird. Dies ist nicht notwendig, wenn mit writeHead gearbeitet wird. Die Angabe ist nur sinnvoll, wenn etwas anderes als den Standardtext gesendet werden soll.

response.statusMessage = 'Not found';

Die Eigenschaft enthält den tatsächlichen Wert nach dem Senden der Antwort.

response.setHeader erzeugt ein Kopffeld oder ersetzt ein Kopffeld, falls es schon vorhanden ist in der Liste der zu sendenden Kopffelder. Sollen mehrere Kopffelder erzeugt werden, kannst du ein Array benutzen. Folgende Syntax gilt:

response.setHeader(name, value)

response.setHeader("Content-Type", "text/html");
response.setHeader("Set-Cookie", ["type=ninja", "language=javascript\
"]);

Jeder kann mit response.headersSent ermittelt werden, ob die Kopffelder bereits gesendet worden sind.

response.sendDate ist eine Boolesche Eigenschaft die anzeigt, ob das Kopffeld Date erzeugt werden soll. Sollte dieses Kopffeld bereits manuell eingetragen sein, wird der manuelle Eintrag nicht überschrieben.

response.getHeader list ein Kopffeld, solange es noch nicht gesendet wurde. Nach dem Senden ist kein Zugriff mehr möglich. Der Name berücksichtigt Groß- und Kleinschreibung nicht – intern sind alle Kopffeldnamen kleingeschrieben. Die Syntax dieser Methode ist wie folgt:

response.getHeader(name)

var contentType = response.getHeader('content-type');

response.removeHeader entfernt ein Kopffeld, solange es noch nicht gesendet wurde:

response.removeHeader("Content-Encoding");

Die Methode response.write schreibt eine Menge an Daten. Das führt dazu, dass implizit festgelegte Kopffelder gesendet werden, weil diese vor den Daten übertragen werden müssen. Wenn zuvor response.writeHead() benutzt wurde, werden die dort definierten Kopffelder benutzt.

response.write(chunk[, encoding][, callback])

Die Methode kann mehrfach aufgerufen werden, um Daten blockweise (chunks) zu übertragen. Der Parameter chunk kann eine Zeichenkette oder ein Byte-Stream sein. Wenn die Daten eine Zeichenkette sind, bestimmt der Parameter encoding, wie diese in Bytes konvertiert werden. Der Standardwert ist ‘utf-8’. Die Rückrufmethode callback wird aufgerufen, wenn die Daten gesendet wurden.

Die Methode gibt true zurück, wenn die Daten an den internen Puffer übergeben wurden. false wird zurückgegeben, wenn Daten im Speicher verblieben sind. ‘drain’ wird erzeugt, wenn der Puffer wieder leer ist.

Mit response.addTrailers(headers) werden Kopffelder an das Ende der Nachricht angehängt. Das geht nur bei Daten, die in Chunks geliefert werden.

response.writeHead(200, { 'Content-Type': 'text/plain',
                          'Trailer': 'Content-MD5' });
response.write(fileData);
response.addTrailers({
   'Content-MD5': "7895bf4b8828b55ceaf47747b4bca667"
});
response.end();

Mit response.end wird mitgeteilt, dass die Übertragung beendet wird. Diese Methode muss immer aufgerufen werden.

response.end([data][, encoding][, callback])

Wenn Daten angegeben sind, wird intern response.write(data, encoding) aufgerufen. Die Rückruffunktion wird aufgerufen, wenn alle Daten gesendet worden sind.

8.2.5 Klasse http.ClientRequest

Eine Instanz dieser Klasse wird durch http.request() erstellt. Dies ist das Anforderungsobjekt. Die Kopffelder sind danach noch änderbar mit den Methoden setHeader(name, value), getHeader(name) und removeHeader(name). Node ist in diesem Fall der Client, der Anfragen an einen anderen Server sendet.

Um die Antwort auf die erzeugte und gesendete Anfrage zu bekommen, übergibt man eine Ereignisbehandlungsfunktion für das Ereignis response. Das Ereignis gibt eine Instanz der Klasse IncomingMessage zurück. Sollte die Antwort Daten enthalten, kann mit dem Ereignis data darauf zugegriffen werden. Alternativ kann auf das Ereignis readable gelauscht werden und dann werden die Daten aktiv mit response.read() gelesen.

 1 var http = require('http');
 2 var net = require('net');
 3 var url = require('url');
 4 
 5 // Proxy für Tunnel erstellen
 6 var proxy = http.createServer(function (req, res) {
 7   res.writeHead(200, {'Content-Type': 'text/plain'});
 8   res.end('okay');
 9 });
10 proxy.on('connect', function(req, cltSocket, head) {
11   // Ursprünglichen Server verbinden
12   var srvUrl = url.parse('http://' + req.url);
13   var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, 
14                               function() {
15     cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
16                     'Proxy-agent: Node-Proxy\r\n' +
17                     '\r\n');
18     srvSocket.write(head);
19     srvSocket.pipe(cltSocket);
20     cltSocket.pipe(srvSocket);
21   }); // Ende function
22 });
23 
24 // Proxy läuft jetzt
25 proxy.listen(1337, '127.0.0.1', function() {
26 
27   // Anforderung erstellen
28   var options = {
29     port: 1337,
30     hostname: '127.0.0.1',
31     method: 'CONNECT',
32     path: 'www.google.com:80'
33   };
34 
35   var req = http.request(options);
36   req.end();
37 
38   req.on('connect', function(res, socket, head) {
39     console.log('got connected!');
40 
41     // Anforderung über Tunnel
42     socket.write('GET / HTTP/1.1\r\n' +
43                  'Host: www.google.com:80\r\n' +
44                  'Connection: close\r\n' +
45                  '\r\n');
46     socket.on('data', function(chunk) {
47       console.log(chunk.toString());
48     });
49     socket.on('end', function() {
50       proxy.close();
51     });
52   });
53 });

Ein weiteres Ereignis muss gegebenenfalls behandelt werden: upgrade. Die Rückruffunktion hat folgende Signatur:

function (response, socket, head)

Ein Upgrade ist erforderlich, wenn der Client das Protokoll wechseln möchte, beispielsweise von HTTP 1.1 auf HTTP 2.0 oder auf WebSockets.

 1 var http = require('http');
 2 
 3 // Create an HTTP server
 4 var srv = http.createServer(function (req, res) {
 5   res.writeHead(200, {'Content-Type': 'text/plain'});
 6   res.end('okay');
 7 });
 8 srv.on('upgrade', function(req, socket, head) {
 9   socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
10                'Upgrade: WebSocket\r\n' +
11                'Connection: Upgrade\r\n' +
12                '\r\n');
13 
14   socket.pipe(socket); // Echo zurück
15 });
16 
17 // now that server is running
18 srv.listen(1337, '127.0.0.1', function() {
19 
20   // make a request
21   var options = {
22     port: 1337,
23     hostname: '127.0.0.1',
24     headers: {
25       'Connection': 'Upgrade',
26       'Upgrade': 'websocket'
27     }
28   };
29 
30   var req = http.request(options);
31   req.end();
32 
33   req.on('upgrade', function(res, socket, upgradeHead) {
34     console.log('got upgraded!');
35     socket.end();
36     process.exit(0);
37   });
38 });

Das Ereignis continue tritt auf, wenn der Server ein 100 Continue sendet, was meist eine Reaktion auf die Anforderung Expect: 100-continue ist. Dies ist die Aufforderung für den Client, dass die Daten der Nachricht gesendet werden dürfen.

Mit request.flushHeaders() steht eine Methode zur Verfügung, die die Kopffelder aktiv sendet. Normalerweise puffert Node Kopffelder und sendet diese nicht sofort, wenn sie definiert werden. Die Pufferung dient der Optimierung, sodass alle Kopffelder idealerweise in ein TCP-Paket passen. Mit flush() und flushHeaders() wird der Optimierungsmechanismus übergangen.

Das eigentlichen Schreiben der Daten erledigt request.write(chunk[, encoding][, callback]) durch blockweises (chunk) Senden der Daten. Es sollte das Kopffeld [‘Transfer-Encoding’, ‘chunked’] benutzt werden, um der Gegenstelle anzuzeigen, dass mit Blöcken gearbeitet wird.

Das Argument chunk kann ein Buffer oder eine Zeichenkette sein. Die Rückruffunktion wird aufgerufen, wenn die Daten gesendet worden sind.

Mit request.end([data][, encoding][, callback]) wird die Anforderung beendet. Wenn Teile der Daten noch nicht gesendet worden sind, wird ein flush erzwungen. Wurden Blöcke benutzt, wird nun die finale Sequenz ‘0\r\n\r\n’ gesendet.

Mit Daten ist das Ergebnis identisch mit dem Aufruf von request.write(data, encoding), gefolgt von request.end(callback). Die Rückruffunktion wird aufgerufen, wenn die Daten gesendet worden sind.

Mit request.abort() kann die Anforderung abgebrochen werden. Mit request.setTimeout(timeout[, callback]) wird der Timeout-Wert festgelegt.

8.2.5.1 http.IncomingMessage

Eine eintreffende Nachricht vom Typ IncomingMessage wird durch http.Server oder http.ClientRequest erzeugt. Das Objekt wird als erstes Argument des request- bzw. response-Ereignisses übergeben. Das Objekt implementiert einen lesbaren Stream sowie einige weitere Methoden und Eigenschaften.

Mit dem Ereignise close wird angezeigt, dass die Verbindung geschlossen wurde. Dieses Ereignis kann nur einmal auftreten.

Die Eigenschaft message.httpVersion zeigt an, welche HTTP-Version benutzt wurde. Das ist entweder ‘1.1’ oder ‘1.0’ usw. Zum Zugriff auf die Versionsdetails dienen response.httpVersionMajor und response.httpVersionMinor.

Die Kopffelder lassen sich über message.headers auslesen. Kopffelder sind intern immer mit Kleinbuchstaben bezeichnet. Die Ausgabe mittel console.log(request.headers); erzeugt etwa folgendes JSON-Objekt:

1 { 
2   'user-agent': 'curl/7.22.0',
3   host: '127.0.0.1:8000',
4   accept: '*/*' 
5 }

Wenn du die Kopffelder direkt, ohne die Behandlung innerhalb von Node, lesen möchtest, eignet sich message.rawHeaders. Interessant hier ist, dass dies kein Verzeichnis mit Schlüssel-/Wertepaaren ist, sondern ein Array, indem abwechseln die Kopffelder und deren Werte stehen.

 1 [ 
 2   'user-agent',
 3   'this is invalid because there can be only one',
 4   'User-Agent',
 5   'curl/7.22.0',
 6   'Host',
 7   '127.0.0.1:8000',
 8   'ACCEPT',
 9   '*/*' 
10 ]

Im end-Ereignis (und nur dort) lassen sich mit message.trailers und message.rawTrailers die Trailer einer in Blöcken (chunks) übertragenen Nachricht abfragen. Mittels Trailer werden blockweise übertragene Nachrichten korrekt zusammengesetzt.

Eine zeitliche Begrenzung der Verarbeitung der Nachricht kann mit message.setTimeout(msecs, callback) erreicht werden. Die Angabe der Zeit erfolgt in Millisekunden, nach Ablauf wird callback aufgerufen.

Das benutzte HTTP-Verb kann der Eigenschaft message.method entnommen werden. In message.url steht der URL der Anforderung. Diese Eigenschaften funktionieren nur, wenn das Objekt von http.Server stammt. Folgender Anforderung soll als Beispiel dienen:

1 GET /status?name=ryan HTTP/1.1\r\n
2 Accept: text/plain\r\n
3 \r\n

In request.url steht dann: ‘/status?name=ryan’

Zum Verarbeiten des URL dient parse:

1 var url = require('url');
2 console.log(url.parse('/status?name=ryan'));

Folgende Ausgabe wird erzeugt:

1 { 
2   href: '/status?name=ryan',
3   search: '?name=ryan',
4   query: 'name=ryan',
5   pathname: '/status' 
6 }

Die Verarbeitung des Querystring kann in einem weiteren Schritt erfolgen:

1 var url = require('url');
2 console.log(url.parse('/status?name=ryan', true));

Folgende Ausgabe wird erzeugt:

1 { 
2   href: '/status?name=ryan',
3   search: '?name=ryan',
4   query: { name: 'ryan' },
5   pathname: '/status' 
6 }

Der Statuscode, der bei der Beantwortung der Nachricht benutzt wird, steht in message.statusCode, der passenden Text dazu in message.statusMessage. Der Code ist der dreistellige HTTP-Code, z.B. 404. Dieser Wert ist nur ereichbar, wenn das Objekt von http.ClientRequest stammt.

Mittels message.socket besteht Zugriff auf das net.Socket-Objekt, dass der benutzten Verbindung zugeordnet ist.

8.2.6 HTTPS

HTTPS ist HTTP, das auf TLS (Transport Layer Security) aufsetzt. Die aktuelle TLS-Version entspricht dem früheren Standard SSL 3.0, TLS ist der Nachfolger von SSL.

Wird HTTPS benutzt, so kannst du mit request.connection.verifyPeer() und request.connection.getPeerCertificate() die Authentifizierungsdaten des Clients ermitteln.

Der Server wird wie bei HTTP folgendermaßen erstellt:

https.createServer(options[, requestListener])

 1 // Abruf: https://localhost:8000/
 2 var https = require('https');
 3 var fs = require('fs');
 4 
 5 var options = {
 6   key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
 7   cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
 8 };
 9 
10 https.createServer(options, function (req, res) {
11   res.writeHead(200);
12   res.end("hello world\n");
13 }).listen(8000);
14 Or
15 
16 var https = require('https');
17 var fs = require('fs');
18 
19 var options = {
20   pfx: fs.readFileSync('server.pfx')
21 };
22 
23 https.createServer(options, function (req, res) {
24   res.writeHead(200);
25   res.end("hello world\n");
26 }).listen(8000);

Die benutzten Methoden und Eigenschaften gleichen weitgehend denen des Moduls ‘http’.

 1 var https = require('https');
 2 
 3 var options = {
 4   hostname: 'encrypted.google.com',
 5   port: 443,
 6   path: '/',
 7   method: 'GET'
 8 };
 9 
10 var req = https.request(options, function(res) {
11   console.log("statusCode: ", res.statusCode);
12   console.log("headers: ", res.headers);
13 
14   res.on('data', function(d) {
15     process.stdout.write(d);
16   });
17 });
18 req.end();
19 
20 req.on('error', function(e) {
21   console.error(e);
22 });

Das Argument options hat gegenüber ‘http’ weitere Optionen:

var options = {
  hostname: 'encrypted.google.com',
  port: 443,
  path: '/',
  method: 'GET',
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
};
options.agent = new https.Agent(options);

var req = https.request(options, function(res) {
  ...
}

Du kannst dies auch ohne Agent-Objekt nutzen:

var options = {
  hostname: 'encrypted.google.com',
  port: 443,
  path: '/',
  method: 'GET',
  key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
  cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem'),
  agent: false
};

var req = https.request(options, function(res) {
  ...
}

8.3 Dateien und Pfaden

Node kann über die entsprechenden Module direkt auf Dateien zugreifen und alle typischen Operationen auf diesen Dateien sowie auf Pfaden und Ordnern ausführen.

8.3.1 Zugriff auf das Dateisystem

Der Dateisystemzugriff unter Node erfolgt über das Modul fs. Alle Aufrufe können sowohl synchron als auch asynchron erfolgen. Während für clientseitige Skripte grundsätzlich nur asynchrone Aufrufe sinnvoll sind, kann dies auf dem Server etwas anders betrachtet werden. Da das Ergebnis einer Aktion möglicherweise das Aussenden von JSON oder HTML ist, wird in der Regel ohnehin gewartet, bis das Ergebnis vorliegt. Dabei haben asynchrone Aufrufe keinen Vorteil. Wenn Ihre Umgebung allerdings stark belastet ist und Skripte spürbare Laufzeiten haben, so wird Node nur immer eine Anfrage bearbeiten und dann alle synchronen Aktionen für diese ausführen. Alle anderen Anfragen warten solange. Wartet nun seinerseits ein Skript erheblich auf eine Dateioperation, so wird der Prozess insgesamt verlangsamt.

Asynchrone Aufrufe nutzen immer eine Rückruffunktion als letztes Argument. Die Rückruffunktionen haben verschiedene Signaturen. Gemeinsam ist jedoch allen Funktionen, dass das erste Argument der Rückruffunktion ein Ausnahmeobjekt (exception) ist, das Fehler anzeigt. Im Erfolgsfall ist dieses Objekt undefined oder null, sodass ein einfacher Test mit if(!exception) auf Erfolg testet.

Synchrone Aufrufe erzeugen immer sofort eine Ausnahme, wenn ein Fehler auftritt. Nutze hier try/catch zum Behandeln der Fehlerzustände.

Hier ein erstes Beispiel für die asynchrone Nutzung:

var fs = require('fs');

fs.unlink('/tmp/hello', function (err) {
  if (err) throw err;
  console.log('successfully deleted /tmp/hello');
});

Hier ein dasselbe Beispiel für die synchrone Nutzung (beachte den Suffix Sync in Zeile 3):

1 var fs = require('fs');
2 
3 fs.unlinkSync('/tmp/hello');
4 console.log('successfully deleted /tmp/hello');

Asynchrone Aufrufe kehren zu einem nicht deterministischen Zeitpunkt zurück. Wenn Du mehrere Aufrufe startest, ist die Reihenfolge bei der Rückkehr nicht garantiert. Das folgende Beispiel ist deshalb fehleranfällig:

1 fs.rename('/tmp/hello', '/tmp/world', function (err) {
2   if (err) throw err;
3   console.log('renamed complete');
4 });
5 fs.stat('/tmp/world', function (err, stats) {
6   if (err) throw err;
7   console.log('stats: ' + JSON.stringify(stats));
8 });

Es kann hier passieren, dass der Aufruf von fs.stat in Zeile 5 erfolgt, bevor bei der vorherigen Aktion in Zeile 1 das umbenennen mit fs.rename abgeschlossen wurde. Deshalb solltest du mehrere aufeinander aufbauende asynchrone Aufrufe verketten:

1 fs.rename('/tmp/hello', '/tmp/world', function (err) {
2   if (err) throw err;
3   fs.stat('/tmp/world', function (err, stats) {
4     if (err) throw err;
5     console.log('stats: ' + JSON.stringify(stats));
6   });
7 });

Du kannst beim Aufruf mit absoluten oder relativen Pfaden arbeiten. Wenn du mit relativen Pfaden arbeitest, sollte klar sein, dass der Ursprung des aktuellen Verzeichnisses der Prozess ist, in dem das Skript ausgeführt wird. Dies kann mit process.cwd() bestimmt werden. In der Regel ist dies der Node-Core.

Machmal kann es vorkommen, dass du die Aktion zwar ausführst, das Ergebnis aber nicht benötigst. Dann kannst du die Rückruffunktion weglassen. Tritt nun aber ein Fehler auf, fehlt der Zugang zum Ausnahmeobjekt. Um dennoch an diese Fehlermeldung zu gelangen, nutzt du die Umgebungsvariable NODE_DEBUG. Das folgende Skript zeigt, wie das erfolgt:

Datei: script.js
1 function bad() {
2   require('fs').readFile('/');
3 }
4 bad();

Führe das Skript nun folgendermaßen aus:

$ env NODE_DEBUG=fs node script.js

Es erfolgt folgende Ausgabe:

fs.js:66
        throw err;
              ^
Error: EISDIR, read
    at rethrow (fs.js:61:21)
    at maybeCallback (fs.js:79:42)
    at Object.fs.readFile (fs.js:153:18)
    at bad (/path/to/script.js:2:17)
    at Object.<anonymous> (/path/to/script.js:5:1)
    <etc.>

Dies gelingt freilich nur, wenn der Pfad wirklich nicht gelesen werden kann – in dem Beispiel wird auf die Root ‘/’ zugegriffen.

8.3.2 Funktionen für den Dateizugriff

Dieser Abschnitt zeigt die wichtigsten Dateizugriffsfunktionen. Hier werden nur die asynchronen Methoden gezeigt. Die meisten Methoden existieren auch synchron. Sie haben dann den Suffix ‘Sync’ im Namen (rename versus renameSync). Bei den synchronen Methoden entfällt die Rückruffunktion.

fs.rename(oldPath, newPath, callback) benennt eine Datei um. Mit fs.ftruncate(fd, len, callback) leerst du eine Datei. Dabei wird entweder ein Dateibeschreibungsobjekt benutzt oder der Pfad zur Datei.

Die folgende Funktionsgruppe setzt den Eigentümer einer Datei:

fs.fchown(fd, uid, gid, callback) fs.chown(path, uid, gid, callback) fs.lchown(path, uid, gid, callback)

Dabei wird entweder ein Dateibeschreibungsobjekt benutzt oder der Pfad zur Datei. Die folgende Gruppe setzt Rechte auf eine Datei:

fs.fchown(fd, mode, callback) fs.chown(path, mode, callback) fs.lchown(path, mode, callback)

Dabei wird entweder ein Dateibeschreibungsobjekt benutzt oder der Pfad zu Datei.

Mit den Folgenden ermittelst du Informationen über eine Datei:

fs.fstat(fd, callback) fs.stat(path, callback) fs.lstat(path, callback)

Die Rückruffunktion hat zwei Argumente, err und stats. stats ist vom Typ fs.Stats. lstat verarbeitet bei symbolischen Links den Link selbst, nicht das Ziel des Links.

Mit diesem ermittelst du den echten Pfad einer Datei:

fs.realpath(path[, cache], callback)

1 var cache = {'/etc':'/private/etc'};
2 fs.realpath('/etc/passwd', cache, function (err, resolvedPath) {
3   if (err) throw err;
4   console.log(resolvedPath);
5 });

Die Methode fs.unlink(path, callback) löscht eine Datei. Mit fs.rmdir(path, callback) wird ein Ordner entfernt. Die Rückruffunktion hat keine zusätzlichen Argumente.

Mit fs.mkdir(path[, mode], callback) wird ein Ordner erzeugt. Der Zugriff auf den Ordner wird mit 0777 (alle haben alle Rechte) festgelegt.

fs.readdir(path, callback) dient dazu, einen Ordner zu lesen und alle Dateien darin als Dateiinformation in ein Array zu legen. Die speziellen Ordner ‘.’ und ‘..’ werden nicht mit aufgenommen.

Die Methode fs.close(fd, callback) schließt eine geöffnete Datei. Die Rückruffunktion hat keine zusätzlichen Argumente. Folgendes öffnet eine Datei dagegen zum Zugriff:

fs.open(path, flags[, mode], callback)

Das Argument flags hat folgende Bedeutung:

mode setzt die Zugriffsrechte, wenn die Datei erzeugt wird. Der Standardwert ist 0666, schreiben und lesen.

Der Zeitstempel einer Datei kann mit fs.utimes(path, atime, mtime, callback) bzw. mit fs.futimes(fd, atime, mtime, callback) geändert werden.

Das Schreiben von Daten erfolgt mit fs.write(fd, buffer, offset, length[, position], callback). buffer liefert die Bytes, position die Position, aber der geschrieben wird, offset dagegen die Position im Puffer. Die Rückruffunktion gibt die geschriebenen Bytes an, einmal die Anzahl und den Puffer selbst. Alternativ kann fs.write(fd, data[, position[, encoding]], callback) benutzt werden.

Das Lesen von Daten erfolgt mit einem Beschreibungsobjekt fd mittels fs.read(fd, buffer, offset, length, position, callback):

Die Rückruffunktion gibt die Anzahl der wirklich gelesenen Bytes und den Puffer an.

Direkt auf einer Datei wird mit fs.readFile(filename[, options], callback) gearbeitet.

1 fs.readFile('/etc/passwd', function (err, data) {
2   if (err) throw err;
3   console.log(data);
4 });

Das Schreiben auf eine Datei erfolgt mit fs.writeFile(filename, data[, options], callback).

1 fs.writeFile('message.txt', 'Hello Node', function (err) {
2   if (err) throw err;
3   console.log('It\'s saved!');
4 });

Mittels fs.appendFile(filename, data[, options], callback) wird direkt an eine existierende Datei angehängt.

1 fs.appendFile('message.txt', 'data to append', 
2               function (err) {
3   if (err) throw err;
4     console.log('The "data to append" was appended to file!');
5   });

Die Methode fs.watch(filename[, options][, listener]) dient dazu, eine Datei auf Umbenennungen hin zu überwachen. Die Methode gibt eine Instanz vom Typ fs.FSWatcher zurück.

1 fs.watch('somedir', function (event, filename) {
2   console.log('event is: ' + event);
3   if (filename) {
4     console.log('filename provided: ' + filename);
5   } else {
6     console.log('filename not provided');
7   }
8 });

Es gibt eine Methode fs.exists(path, callback) die testet, ob eine Datei existiert. Die Benutzung ist jedoch nicht empfehlenswert.

Mit fs.access(path[, mode], callback) testest du die Zugriffsrechte eines des aktuellen Benutzers. Die Rückgabe enthält Werte aus einer Liste von Konstanten:

1 fs.access('/etc/passwd', fs.R_OK | fs.W_OK, function(err) {
2   util.debug(err ? 'no access!' : 'can read/write');
3 });

8.3.3 Funktionen zum Umgang mit Streams

Stream verarbeiten Daten byteweise, was meist effizienter ist.

Der Aufruf fs.createReadStream(path[, options]) gibt ein ReadStream-Objekt zurück. Das Argument options hat diese Standardwerte:

1 { 
2   flags: 'r',
3   encoding: null,
4   fd: null,
5   mode: 0666,
6   autoClose: true
7 }
fs.createReadStream('sample.txt', {start: 90, end: 99});

Mit fs.createWriteStream(path[, options]) wird ein Stream zum Schreiben erstellt, das Objekt ist vom Typ WriteStream.

1 { 
2   flags: 'w',
3   encoding: null,
4   fd: null,
5   mode: 0666
6 }

8.4 Express

Express ist die Middleware-Komponente einer Node-Applikation. Damit ist die Vermittlungsschicht zwischen dem Client und dem Backend mit seinen Persistenzfunktionen gemeint. Kernaufgabe ist das Routing.

8.4.1 Installation

Voraussetzung für Express ist eine funktionierende Node-Umgebung. Liegt diese vor, kannst du eine erste Applikation erstellen. Der hier gezeigte Ablauf stellt Express bereit, die eigentliche Infrastruktur musst du aber manuell erstellen. Im Abschnitt Applikationsstruktur findest du Informationen, wie der Express-Generator eingesetzt werden kann, um dies zu vereinfachen.

Zuerst wird ein Ordner für die Applikation geschaffen:

1 mkdir SimpleApp
2 cd SimpleApp

Mit npm init wird dann eine Datei package.json erzeugt. Damit werden die Applikation und ihre Abhängigkeiten beschrieben.

npm init

Die Angaben für die Beschreibungsdatei werden im Dialog abgefragt. In den meisten Fällen ist es in Ordnung die Standards zu übernehmen. Drückst du also einfach mehrfach ENTER, außer für die Option entry point. Hier gib folgendes ein:

entry point: app.js

Dies bestimmt, dass die Startdatei, also der Beginn der Applikation, app.js ist. Du kannst natürlich jeden Namen wählen.

Nun wird Express installiert und in die Liste der Abhängigkeiten (Option --save) aufgenommen. Füge gegebenenfalls noch die Option -g hinzu, um Express global verfügbar zu machen. Das ist sinnvoll, wenn du planst, weitere Projekte mit Node zu entwickeln.

1 $ npm install express --save
Abbildung: Interaktive Installation (Ubuntu)
Abbildung: Interaktive Installation (Ubuntu)

8.4.2 Applikationsstruktur

Express liefert eine fertige Applikationsstruktur. Mit der Installation steht nicht nur das Express-Modul bereit, sondern die fertige Ordnerstruktur kann mit nur einem Befehl erstellt werden. Du musst dies jedoch nicht nutzen. Es ist durchaus möglich, eine Applikation ganz einfach auf einer einzigen Datei aufbauend zu erstellen.

Bei der Installation im vorherigen Abschnitt wurde bei der Initialisierung gesagt, dass die Startdatei app.js lautet. Diese könnte nun folgendermaßen aussehen:

 1 var express = require('express');
 2 var app = express();
 3 
 4 app.get('/', function (req, res) {
 5   res.send('Hallo Express!');
 6 });
 7 
 8 var server = app.listen(3000, function () {
 9   var host = server.address().address;
10   var port = server.address().port;
11 
12   console.log('Ich höre  auf http://%s:%s', host, port);
13 });

Hier wird zuerst Express selbst eingebunden und mit dem Konstruktor-Aufruf eine Applikation app erstellt. Dann wird eine Route festgelegt, sie Stammroute “/”. Alle anderen Aufrufe führen zu einem HTTP-Fehler 404 (Nicht gefunden). Dann wird der Endpunkt bestimmt, hier der Port 3000 auf dem lokalen System (Zeile 8). Trifft nun ein HTTP-Request ein, wird die Funktion der passenden Route ausgeführt. Im Beispiel wird dann der Text “Hallo Express!” ausgegeben. HTML gibt dieses Skript noch nicht zurück, dies muss alles separat erledigt werden. Es handelt sich jedoch bereits um eine korrekte HTTP-Kommunikation

8.4.2.1 Der Express-Generator

Zum Erzeugen einer Applikation kann der Express-Generator eingesetzt werden. Dieser steht als weiteres NPM-Paket zur Verfügung.

$ npm install express-generator -g

Der Generator verfügt über einige Optionen, erzeugt aber auch ohne weitere Angaben eine sinnvolle Umgebung.

Tabelle: Optionen des Express-Generators
Option Bedeutung
-v, –version Version
-e, –ejs EJS-Engine (siehe www.embeddedjs.com)
-hbs Handlebars-Engine
-H, –hogan Hogan-Engine (www.hogan.js)
-c, –css [CS] CSS-Precompiler
-f, –force Erzwinge Dateien in nicht-leeren Ordnern

Die Standard-Template-Engine ist Pug.

Der CSS-Precompiler kann einer der folgenden sein (Name und in Klammern die zu benutzende Option (option)):

Ohne Angabe wird einfaches CSS erwartet.

LESS oder SASS

In diesem Werk wird mit LESS (http://lesscss.org) gearbeitet. Prinzipiell ist das egal, wenn du bereits einen Favoriten hast, nutze diesen. Wenn beides neu ist, dann wirst mit LESS möglicherweise etwas glücklicher am Anfang, da es einfacher und weiter verbreitet ist (das heißt mehr Quellen zum Lernen und weniger Aufwand). Profis greifen dagegen oft zu SASS (http://sass-lang.com). Die Markttendenz Ende 2017 geht in Richtung SASS.

Der Generator erzeugt auch das Stammverzeichnis der Applikation, sodass du am besten im übergeordneten Verzeichnis beginnst:

1 express PortalApp
2 cd PortalApp
3 npm install

Mit dieser Befehlsfolge wird eine Applikation mit dem Namen PortalApp im Ordner PortalApp erstellt. Nun wird die Applikation im Debug-Modus gestartet:

set DEBUG=PortalApp & npm start

Die Standardadresse ist http://localhost:3000. Der Webserver basiert auf Node und weitere Einstellungen am Betriebssystem sind nicht erforderlich. Du musst hier weder IIS noch Apache oder sonst einen Server bereitstellen – es funktioniert, einfach so.

Folgende Struktur entsteht standardmäßig:

Abbildung: Struktur, die der Generator anlegt
Abbildung: Struktur, die der Generator anlegt

8.4.3 Routing in Node-Applikationen

Das Routing stellt einen Zusammenhang zwischen einem URL und einer ausführenden Instanz (Methode oder Modul) her. Immer wenn eine Applikationen mehr als eine Seite ausliefert, kommt Routing ins Spiel. Das trifft auch auf Single-Page-Applikationen (SPA) zu. Denn beim Stand heutiger Browser bist du gut beraten, wenn nicht alles in eine einzige Seite gepresst wird. Das grobe Raster der Applikation ist besser in mehreren serverseitigen Modulen aufgehoben, die ihrerseits jeweils gut als SPA ausgeführt werden können.

Das Routing kommt natürlich zu ganz anderer Bedeutung, wenn keine SPA erstellt wird. Dann geht es praktisch darum, die Auslieferung jeder einzelnen Seite zu steuern. Neben den Seiten ansich kümmert sich das Routing dann auch um die Parameter, die als Teil des URL mitgeliefert werden und den ausführenden Methoden zugeführt werden müssen.

8.4.3.1 Routing in Express

Wenn mehr Seiten einer Applikation hinzugefügt werden, werden mehr Routen benötigt. Dazu dient der Express Router. Dieser wird hier noch umfassend behandelt. Routen liefern aber nicht nur fertige Seiten aus. Wird ein Teil der Applikation im Single Page-Stil (SPA) entwickelt, nutze Express, um die Routen für ihre clientseitig programmierten Abrufe zu erstellen. Dies kann beispielsweise mit AngularJS erfolgen. Express bildet dann ein RESTful-Backend für AngularJS ab.

Erst die Kombination von serverseitiger Technologie und clientseitigen Elementen macht eine moderne Webapplikation aus.

8.4.3.2 Der Express Router

Der Express Router ist ein reines Routingmodul ohne viele Extras. Es gibt keine explizite Unterstützung von Views oder vordefinierte Einstellungen. Es gibt aber rudimentäre APIs wie use(), get(), param() und route(). Es gibt verschiedene Möglichkeiten, den Router zu benutzen. Die Nutzung von get() ist dabei nur eine Variante. Die folgende Beispielapplikation nutzt diese und einige andere Techniken. Am Ende des Texts findest du eine Beschreibung der gesamten API.

8.4.4 Eine Beispielapplikation

Die Beispielapplikation verfügt über einige Techniken, die in der Praxis sinnvoll eingesetzt werden können:

Nun wurde bereits mehrfach der Begriff Middleware benutzt. Doch um was handelt es sich dabei eigentlich im Zusammenhang mit Express?

8.4.4.1 Middleware – die Vermittlerschicht

Der Name Middleware ist trefflich gewählt. Die hier platzierten Funktionen werden nach dem Eintreffen der Anfrage vom Client und vor der Weiterleitung zur Beantwortung ausgeführt. Sie haben also maßgeblichen Einfluss auf die Verarbeitung der Anfrage. Eine Anwendung ist die Protokollierung von Anfragen. Diese finden in der Middleware statt, ohne Rücksicht auf die Arbeitsweise der anderen Komponenten – transparent und im Hintergrund.

8.4.4.2 Grundlegende Routen

Die Route zur Homepage wurde bereits definiert. Diese wie alle anderen Routen werden in der Datei app.js definiert. Diese Datei ist im Projekt der beste Platz, solange die Anzahl der Routen überschaubar ist. Da bei einer Single-Page-Applikation (SPA) nur das grobe Raster der Routen auf dem Server ausgeführt wird, ist dies in Ordnung. AngularJS kümmert sich dann um die Routen auf der Clientseite und regelt die Abfrage spezifischer Teil-Sichten mittels Parametern.

Definierte Routen reagieren auf spezifische Pfade und HTTP-Verben wie GET, POST, PUT/PATCH oder DELETE. Das funktioniert – mit oder ohne RESTful-Aktionen – solange nur eine Handvoll Routen benötigt werden.

Nun kann es vorkommen, dass doch komplexere Routen erforderlich werden. Komplexe Websites haben nicht nur einen Bereich, sondern auch Backend-Funktionen, Administrationsbereiche, Im- und Export, Reporting und vieles mehr. Jeder Bereich kann über unzählige Routen verfügen.

Die Funktion express.Router() realisiert eine Art Mini-Applikation. Du erzeugst damit eine Instanz des Routers und definieren für diese Instanz einige Routen.

Listing: app.js
 1 // Die Applikationsinstanz wird gebildet
 2 var express = require('express');
 3 var app = express();
 4 
 5 // Eine neue Instanz des Routers wird erstellt
 6 var adminRouter = express.Router();
 7 
 8 // Die Admin-Site (http://localhost:3000/admin)
 9 adminRouter.get('/', function(req, res) {
10   res.send('Startseite des Admin-Bereichs!');
11 });
12 
13 // Die Benutzer-Site (http://localhost:3000/admin/users)
14 adminRouter.get('/users', function(req, res) {
15   res.send('Alle Benutzer anzeigen!');
16 });
17 
18 // Die Artikel-Seite (http://localhost:3000/admin/article)
19 adminRouter.get('/article', function(req, res) {
20   res.send('Alle Artikel anzeigen!');
21 });
22 
23 // Zuweisen der Routen an die Applikation
24 app.use('/admin', adminRouter);
25 
26 // Der Server
27 var server = app.listen(3000, function() {
28   console.log('Server gestartet');
29 });

Die Routen werden quasi isoliert erstellt und dann als Gruppe der Applikation zugewiesen. Die Pfade werden dabei addiert. Der Stammpfad wird durch die Methode use bestimmt. Die Anweisung könnte auch folgendermaßen aussehen:

app.use('/app', router)

Solche Miniapplikationen lassen sich mehrfach zuweisen und damit allein gewinnst du einiges an Übersichtlichkeit. Logisch getrennte Bereiche, wie beispielsweise Views und REST-API lassen sich nun auch im Quellcode sauber auseinanderhalten.

8.4.4.3 Die Router-Middleware (router.use())

Generell greift die Middleware vor der eigentlichen Verarbeitung ein. Dies ist sinnvoll für eine Reihe von Aufgaben:

Die Definition der Funktionen erfolgt in eben der Reihenfolge wie sie später benutzt werden. Die Einrichtung erfolgt nach der Erstellung der Applikation und vor dem Zuweisen der Routen. Dass folgende Beispiel zeigt, wie sämtliche Anfragen auf der Konsole ausgegeben werden.

1 // Funktion, die auf jede Anfrage reagiert
2 adminRouter.use(function(req, res, next) {
3   // Konsolenausgabe
4   console.log(req.method, req.url);
5   // Weiter mit der regulären Verarbeitung
6   next();
7 });

Entscheidend ist der Aufruf der Methode next(). Damit wird Express mitgeteilt, dass die Methode abgearbeitet wurde und mit der regulären Verarbeitung fortgesetzt werden kann. adminRouter.use() definiert die Middleware-Funktion. Die eigentliche Funktionalität ist selbst zu implementieren und damit reines JavaScript.

Die Reihenfolge der Anmeldung der Funktionen bestimmt auch die Reihenfolge der Verarbeitung. Nach der Route ist kein Platz für die Middleware-Funktionen, weil die Verarbeitung der Anfrage dort mit dem Aussenden der Daten endet.

8.4.4.4 Routen strukturieren

Bisher wurde bereits gezeigt, wie Routen abschnittsweise vergeben werden können. Die Vorgehensweise ist bei den meisten Projekten ähnlich. Die Startseite mit ihren wichtigsten Links ist ein Bereich, die Administration ein anderer. Eine API – RESTful oder nicht – sollte immer separat geführt werden. Damit hast du genug Spielraum, um bei Erweiterungen nicht die Übersicht zu verlieren. Die Festlegung der Bereiche sieht dann so aus:

1 app.use('/', basicRoutes);
2 app.use('/admin', adminRoutes);
3 app.use('/api', apiRoutes);
8.4.4.5 Routen mit Parametern (/hello/:id)

Der Aufruf einer Seite alleine reicht meist nicht aus. Werden Daten aus Datenbanken abgerufen, müssen Parameter übertragen werden. Der Aufbau der URL ist nahezu beliebig. Du musst allerdings die Grenzen von HTTP beachten. Die Länge einer URL ist auf 2000 Zeichen begrenzt. Außerdem lädt eine komplexe URL zum Spielen ein, die Angaben sind bei den meisten Browsern klar sichtbar und leicht zu manipulieren. Je komplexer die URL desto höher ist der Aufwand für die Validierung der Parameter.

Wenn du Daten aus Datenbanken abrufst, bietet es sich an alle Abrufe auf den Primärschlüssel zu beschränken. Das führt in der Geschäftslogik möglicherweise dazu, das Daten aus verbundenen Tabellen oder Dokumenten erneut geladen werden. Im Ausliefern solcher Anfragen sind Datenbanken aber richtig gut und die Vereinfachung bei der Gestaltung des Servercodes ist fast immer wertvoller. Nenne ihren primären Parameter dann konsequenterweise immer id.

In der Beschreibung der Route werden Parameter mit einem Doppelpunkt eingeleitet:

1 adminRouter.get('/users/:id', function(req, res) {
2   res.send('Benutzer-ID: ' + req.params.id + '!');
3 });

Der Router erkennt dies und überträgt die Werte in ein Objekt mit dem Namen params, das Teil des Anforderungsobjekts req ist. Dort stehen die Parameter als Eigenschaften zur Verfügung. Die URL für dieses Beispiel sieht folgendermaßen aus:

http://localhost:3000/admin/users/123

Der Pfad-Abschnitt admin wurde im Router selbst definiert, der spezifische Pfad legt user fest und 123 wird an die Eigenschaft id übergeben. Der Doppelpunkt dient der Erkennung und ist nicht Teil des Pfades.

Durch die hohe Anfälligkeit für Manipulationen müssen Parameter immer validiert werden. An dieser Stelle kommt wieder die Middleware-Schicht ins Spiel. Sie stellt eine Methode param() zur Verfügung, der die Parameter übergeben werden, bevor sie der Verarbeitung zugeführt werden.

8.4.4.6 Router-Middleware für Parameter (.param)

Die Funktion param() bildet die Parameter für eine bestimmte Route ab. Auch dieser Aufruf muss vor der Abarbeitung der Anforderung stehen. Innerhalb der Funktion können dann die Parameter untersucht, modifiziert oder sonstwie behandelt werden.

Das folgende Beispiel zeigt, wie der Parameter id geprüft wird:

Listing: param_sample.js
 1 adminRouter.param('id', function(req, res, next, name) {
 2   console.log('Validierung für ID ' + id);
 3   var id = Number(req.params.id);
 4   if (!id){
 5     // Fehlerbehandlung
 6   } else {
 7     // Ablage des geprüften Wertes
 8     req.id = id;
 9     // Weiter mit Verarbeitung
10     next();
11   }
12 });
13 
14 adminRouter.get('/users/:id', function(req, res) {
15   res.send('ID: ' + req.id + '!');
16 });

Ein gültiger URL ist hier:

http://localhost:3000/admin/users/123

Wie die Fehlerbehandlung hier aussieht, hängt wieder von der Aufgabenstellung ab. Eine Website für (menschliche) Benutzer verlangt sicher andere Reaktionen als eine RESTful-API, die möglichweise auf technische Fehler reagieren muss.

8.4.4.7 Mehrere Routen (app.route())

Die Funktion app.route() ist ein direkter Aufruf des Routers und entspricht dem Aufruf express.Router(). Die Funktion verfügt aber außerdem über die Möglichkeit mehrere Routen in einem Schritt zu erstellen und mehrere Aktionen über eine Route abzubilden. Das letzte vermeidet, dass bei hunderten Aktionen ebenso viele Routen erstellt werden müssen.

Im folgenden Beispiel wird eine Route /login definiert. Auf diese reagieren zwei Methoden. Einmal wird das Verb GET ausgewertet, einmal POST.

Listing: login_sample.js
1 app.route('/login') 
2    .get(function(req, res) {
3       res.send('Das Anmeldeformular.');
4    })
5    .post(function(req, res) {
6       console.log('Anmelden');
7       res.send('Anmeldung verarbeitet!');
8    });

app ist im Beispiel das zentrale Applikationsobjekt und die Definition erfolgt überlicherweise in der Datei app.js.

Die Vorgehensweise ist typisch für alle Arten von Formularen. Wird die Seite im Browser mit http://localhost:3000/login aufgerufen, erzeugt der Browser eine GET-Anfrage. Der Benutzer sieht das Formular und füllt es aus. Er sendet es dann mit der Sende-Schaltfläche (submit) ab. Der Browser erstellt nun eine POST-Anfrage und fügt die Formulardaten an.

Man spricht nun von Aktionen einer Route. Im letzten Beispiel waren es zwei Aktionen. Eine RESTful-API könnte noch auf weitere Verben mit derselben Route reagieren.

8.5 Pug

Pug ist eine Template-Engine für Express, der Middlware- und Routing-Lösung für Node.js. Sie ist der Standard für Express. Wenn du dich also intensiv mit Node.js und Express auseinandersetzt, führt kein Weg an Pug vorbei.

Pug nutzt eine vereinfachte Darstellung der HTML-Seite durch simple Textbefehle. Praktischerweise entsprechen diese den Namen der HTML-Tags. Da HTML eine Hierarchie aufbaut und Pug keine schließenden Tags kennt, muss die Baumstruktur anders entstehen. Pug nutzt dazu Einrückungen im Texteditor. 2 Leerzeichen zeigen an, dass das folgende Element ein Kindelement ist.

8.5.1 Vorbereitung

Pug setzt voraus, dass du mit node.js arbeitest und die Middleware Express nutzt. Der einfachste Weg zu einer funktionierenden Umgebung geht über ein schrittweises Abarbeiten der Bausteine einer node.js-Installation.

Damit steht die Umgebung, und der Beschäftigung mit Pug steht nichts im Weg.

8.5.2 Installationsanleitung

Zur Installation gibt es wenig Besonderheiten. Pug benötigt Express. Falls dies mit der Videoanleitung bereits installiert wurde, kann dieser Schritt überprungen werden. Ansonsten folge der Schrittfolge hier.

Installieren nun das Pug-Paket. Ich habe meine Projekte unter /home/joerg/Apps und für dieses Bändchen dann unter Pug abgelegt. Lege dann einen Ordner views an, in dem eine erste Testseite entstehen wird (sie befinden sich in Apps):

1 mkdir Pug
2 cd Pug
3 npm install express
4 npm install Pug
5 npm init
6 npm install express --save
7 mkdir views
Abbildung: Beschreibung der Applikation
Abbildung: Beschreibung der Applikation

npm init erstellt die Node.js-Applikation. Dabei wird eine Paket-Datei erzeugt, deren Werte interaktiv abgefragt werden. Die erzeugte Paket-Datei kann noch manuell angepasst werden.

Lege in dem neu erstellten Applikationsverzeichnis Pug eine Datei mit dem Namen index.js an. Sie hat folgenden Inhalt:

1 var express = require('express');
2 var app = express();
3 
4 app.get('/', function (req, res) {
5   res.send('Hallo Express!');
6 });
7 
8 var server = app.listen(3000, function () {});

Starte nun den Node-Server:

npm start

Abbildung: Start der Applikation
Abbildung: Start der Applikation

Gib nun im Browser auf dem Entwicklungssystem folgende URL ein: http://localhost:3000. Sie sollten dann die “Hallo Express!”-Ausgabe sehen.

Abbildung: Ausgabe der Seite
Abbildung: Ausgabe der Seite

8.5.3 Applikationsstruktur

Express bietet eine Reihe spannender Funktionen. Ich will hier jedoch nur auf Pug eingehen und deshalb ist das manuelle Erzeugen und nutzen einer View einfacher.

Die einfachste Nutzung von Pug besteht aus zwei Bausteinen. Zum einen die erste View, index.pug:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     h1= title

Zum anderen wird das “Hallo Express”-Beispiel so verändert, dass nun statt des statischen Texts die View benutzt wird:

Datei: index.js
 1 var express = require('express');
 2 var app = express();
 3 app.set('view engine', 'pug');
 4 
 5 app.get('/', function (req, res) {
 6   res.render('index', { 
 7     title: 'Hallo pug!' 
 8   });
 9 });
10 
11 var server = app.listen(3000, function () {});

Zum einen wird hier Pug als Standard vereinbart, sodass keine Dateierweiterung angegeben werden muss und das passende Modul vorab geladen werden kann. Dies passiert durch:

app.set('view engine', 'pug');

Dann wird statt res.send die Funktion res.render benutzt. Der erste Parameter ist der Name der View, der ohne Pfad (standardmäßig wird im view-Ordner gesucht) und ohne Dateierweiterung (standardmäßig wird nun pug benutzt) angegeben werden kann. Der zweite Parameter ist ein Objekt, dass lokale Variablen für die View bestimmt. Jede Eigenschaft des Objekts wird als lokale Variable bereitgestellt. Im Beispiel ist das der Wert title.

8.5.3.1 Pug-Views

Statt HTML schreib ab jetzt die Ansichtsseiten in Pug. Hier noch einmal das eben benutzte Beispiel:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     h1= title

Auf jeder Zeile der View steht zuerst ein HTML-Tag. Statt der Schreibweise in XML-Form (<title></title>) nimmt Pug hier eine vereinfachte Darstellung.

title= title

Der linke Teil ist das HTML-Element. Es folgt ein Gleichheitszeichen, dass die Kodierung bestimmt, also die Behandlung von HTML-spezifischen Entitäten wie < oder >. Dann folgt JavaScript. Da eine lokale Variable mit dem Namen title vereinbart wurde, wird dieser Ausdruck hier hingeschrieben. Analog funktioniert das mit h1, dass unterhalb des body-Elements steht. Der Umgang mit body zielt darauf ab, dass Views üblicherweise auf Stammseiten (Layout- oder Master-Seiten) basieren und der eigentliche Inhalt über die Variable body (rechts im Ausdruck) zugeordnet wird. Da HTML aus einer Seite direkt übernommen werden soll, wird der Operator != benutzt, der nicht codiert.

8.5.3.2 Umgang mit Teil-Ansichten

Teil-Ansichten (partial views) erlauben das Strukturieren von Views. Eine Pug-View sieht beispielsweise folgendermaßen aus:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     include navigation
7     h1= title

Mit dem Befehl include wird eine weitere View eingebunden, navigation.pug. Beachte, dass diese ohne Anführungszeichen und Klammern angegeben wird.

Diese Navigation wird nun in einer weiteren Datei erstellt: views/navigation.pug:

Datei: navigation.pug
1 div#navigation
2   a(href='/') home
8.5.3.3 Umgang mit Layout-Seiten

Eine Layout-Seite ist ein Master, also ein Gerüst einer Seite, die Grundstruktur und unveränderliche Elemente enthält. Dies umfasst beispielsweise die Navigation, das Logo und den Fußbereich. Die eigentliche Seite füllt dann nur einen Inhaltsbereich aus.

Eine einfache Layout-Seite sieht nun folgendermaßen aus:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     include navigation

Dies unterscheidet sich in kaum von dem vorherigen Beispiel. Lediglich das h1-Element am Ende fehlt.

Im nächsten Schritt wird die Inhaltsseite erstellt. Sie heißt views/content.pug:

Datei: content.pug
1 extends index
2 
3 h1= title
4   a(href='http://www.joergkrause.de/') Jörg &lt;Is A Geek&gt; Krause

Sie verweist auf die Layout-Seite. Nun wird das Startskript angepasst, denn Pug rendert zuerst die Inhaltsseite, die ihrerseits die Layout-Seite aufruft.

Datei: index.js
 1 var express = require('express');
 2 var app = express();
 3 app.set('view engine', 'pug');
 4 
 5 app.get('/', function (req, res) {
 6   res.render('content', { 
 7     title: 'Hallo Pug!' 
 8   });
 9 });
10 
11 var server = app.listen(3000, function () {});

Beachte die res.render-Funktion, die nun content statt vorher index aufruft (Zeile 5).

Jetzt kann der node-Server gestartet werden (im Ordner wo die Datei package.json steht):

npm start

Soweit der Standardport nicht anderweitig vergeben wurde zeigt der Browser die gerenderte HTML-Seite nun an:

http://127.0.0.1:3000/

Der Einstiegspunkt ist der Aufruf von res.render mit dem Argument der Inhaltsseite, content.pug. Die Engine sorgt dann für das Laden der Layout-Seite und die Verarbeitung. Der gesamte Vorgang findet also auf dem Server statt.

Abbildung: Ausgabe mit Layout-Seite
Abbildung: Ausgabe mit Layout-Seite

Dabei fällt auf, dass die Navigation verschwunden ist. Das ist das normale Verhalten. Denn nun wurde der Inhalt des body-Elements tatsächlich durch eine Inhaltseite geliefert und damit wird der statische Inhalt überschrieben. Freilich gibt es hier einige Optionen, dieses Verhalten zu verändern. Dies wird in der Sprachreferenz genau beschrieben.

8.5.3.4 Sprachreferenz

Der Einstieg in die online verfügbaren Informationen ist Github. Ergänzend zu diesem Buch findest du eine deutsche Srpachreferenz auf meiner Homepage unter folgender Adresse: