Aus Linux-Magazin 05/2013

Eigene Webanwendungen mit Node.js und Redis programmieren

© Oleg Zhukov, 123RF.com

Javascript auch auf dem Server nutzen, das verspricht das Webserver-Framework Node.js und bietet zahlreichen Erweiterungen und viele Funktionen, aber auch hohe Performance. Dieser Artikel zeigt, wie sich im Verbund mit der Datenbank Redis eine einfache und schnelle Buchungsapplikation entwickeln lässt.

Linux, Apache, MySQL, PHP – LAMP-Anwendungen dominieren das Web. Typischerweise bestehen sie aus einem Bündel von PHP-Programmen, die Daten in MySQL-Datenbanken speichern oder auslesen, auf einem Linux-Server gespeichert sind und Clients über einen Apache-HTTP-Server bedienen.

Doch der Shootingstar unter den Webservern tickt anders: Node.js [1] ist nicht nur ein HTTP-Server, sondern ein ganzes Programmiergerüst, das es erlaubt, hochperformante und skalierbare Webanwendungen in Javascript zu schreiben. Im Gegensatz zu Browserskripten laufen die Javascript-Applikationen wie PHP-Programme auf dem Server.

Knoten ersetzt Lampe

Das folgende Beispiel zeigt die Stärken von Anwendungen in Node.js im Vergleich zu den weitverbreiteten LAMP-Programmen. Die Open-Source-Software, (unter MIT-Lizenz) basiert auf der Chrome-Laufzeitumgebung V8 [2] für Javascript und ist mittlerweile in den Repositories aller gängigen Distributionen enthalten. Listing 1 zeigt die simple Installation der aktuellen Version 0.8.15 unter Ubuntu Quantal Quetzal.

Listing 1

Installation

01 sudo apt-get install python-software-properties
02 sudo add-apt-repository ppa:chris-lea/node.js
03 sudo apt-get update
04 sudo apt-get install nodejs np

Das Design von Node.js ähnelt der Ruby-Bibliothek Eventmachine [3] und der Python-Bibliothek Twisted [4]. Auch in Node.js benötigen Webanwendungen einen eigenen HTTP-Server. Listing 2 zeigt, wie sich der HTTP-Server von Node.js nach Art eines Echo-Servers verwenden lässt.

Listing 2

Node.js als Echo-Server

01 var http = require('http');
02 http.createServer(function(req, resp) {
03   resp.writeHead(200, {'Content-Type': 'text/plain'});
04   resp.end(req.url);
05 }).listen(3000);

Ein Echo-Server

Zeile 1 erzeugt mit der Funktion »require()« eine Referenz auf das im Modul »http« enthaltene Objekt und speichert sie in der gleichnamigen Variablen. In der darauf folgenden Zeile erstellt die Methode »createServer()« ein HTTP-Server-Objekt. Der Aufruf bekommt eine Callback-Funktion übergeben, die der Server immer bei Eingang einer HTTP-Anfrage ausführt. Sie übernimmt mit dem Parameter »req« ein Objekt, das die HTTP-Anfrage, und mit »resp« eines, das die HTTP-Antwort repräsentiert.

In Zeile  3 schreibt die Methode »writeHead()« Werte in den Kopf der HTTP-Antwort. Der Code »200« steht für eine erfolgreiche Anfrage, die Angabe »text/plain« für den Mime-Type der Antwort. In Zeile 4 beendet die Methode »end()« die HTTP-Antwort. Zuvor schreibt das Skript den Pfadanteil der abgefragten URL aus der gleichnamigen Eigenschaft noch in den Körper der HTTP-Antwort. Zeile 5 bindet mit der Methode »listen()« das HTTP-Server-Objekt an den Port 3000. Abbildung 1 zeigt die Antwort des Echo-Servers auf eine HTTP-Anfrage mit dem Kommandozeilentool Curl.

Abbildung 1: Der minimalistische Echo-Server antwortet und gibt den Pfandanteil der als Parameter angegebenen URL zurück.

Abbildung 1: Der minimalistische Echo-Server antwortet und gibt den Pfandanteil der als Parameter angegebenen URL zurück.

Der HTTP-Server von Node.js arbeitet alle Anfragen mit einem einzigen Serverprozess ab. Der Apache-HTTP-Server hingegen verwendet, je nach Modell, pro Anfrage einen Prozess. Abbildung 2 illustriert den von Node.js eingesparten Speicherplatz. Weil in Node.js Aufrufe zumeist asynchron erfolgen, kann der Serverprozess mehrere HTTP-Anfragen gleichzeitig beantworten.

Abbildung 2: Im Vergleich zu Apache spart Node.js mit nur einem Prozess deutlich Speicherplatz.

Abbildung 2: Im Vergleich zu Apache spart Node.js mit nur einem Prozess deutlich Speicherplatz.

Node.js-Anwendungen lassen sich jedoch auch über mehrere Prozesse verteilen. Listing 3 zeigt die Skalierung des HTTP-Servers aus Listing 2 mit der Anzahl der Prozessoren. Zeile 2 bindet zunächst das Modul »cluster« ein, das die eigentliche Skalierung durchführt. Zeile 3 ermittelt daraufhin die Anzahl der Prozessorcores. Die Zeilen 5 bis 7 erzeugen im Masterprozess mit der Methode »fork()« Kopien von sich, die Zeilen 9 bis 12 starten pro Kopie jeweils einen HTTP-Server. Diese übernehmen wechselweise HTTP-Anfragen an den Port 3000. Im Gegensatz zu einer LAMP-Anwendung fasst Node.js hier Server und Anwendung in einem Prozess zusammen.

Listing 3

Skalieren des Echo-Servers

01 var http=require('http'),
02   cluster=require('cluster'),
03   cpus=require('os').cpus().length;
04 if (cluster.isMaster) {
05  for(var i = 0; i < cpus; i++) {
06   cluster.fork();
07  }
08 } else {
09  http.createServer(function(req, resp) {
10   resp.writeHead(200, {'Content-Type': 'text/plain'});
11   resp.end(req.url);
12  }).listen(3000);
13 }

Events

Javascript soll es Entwicklern einfacher machen, auf Benutzereingaben zu reagieren. Beim Einsatz im Browser löst ein Klick auf einen Knopf in einer Webseite ein so genanntes Event aus und der Javascript-Interpreter daraufhin die an das Event gebundene Callback-Funktion. Node.js verwendet Events, um auf asynchrone Lese- und Schreiboperationen zu reagieren, der Objekt-Eventemitter aus dem Modul »events« stellt sie zur Verfügung.

Listing 4 demonstriert das Erzeugen des benutzerdefinierten Events »schawawa« für das Objekt »Custom« . Zeile 1 bindet zunächst das Modul »events« ein. Die Zeilen 4 bis 6 speichern in der Variablen »Custom« eine Konstruktor-Funktion. In Zeile 8 weist die Methode »inherits()« das Objekt »EventEmitter« als Prototyp für alle Objekte aus, die mit der Konstruktor-Funktion aus den Zeilen 4 bis 6 erzeugt werden.

Listing 4

Eventemitter vererbt

01 var events = require('events'),
02   util = require('util');
03
04 var Custom = function() {
05  title:"Custom Object";
06 }
07 //Custom.prototype = new events.Emitter;
08 util.inherits(Custom, events.EventEmitter);
09
10 var custom = new Custom();
11 custom.on('schawawa', function(arg) {
12  console.log('react on schawawa with data: '+arg);
13 });
14
15 setTimeout(function() {
16  custom.emit('schawawa', '\\0000');
17 }, 2000);

Dadurch erbt das Objekt »Custom« in Zeile 10 alle Methoden und Eigenschaften vom »EventEmitter« . Die Methode »on()« vereinbart in den Zeilen 11 bis 13 für das Schawawa-Event eine Callback-Funktion. Die Zeilen 15 bis 17 lösen Schawawa durch Aufruf der Methode »emit()« mit einer Verzögerung von 2000 Millisekunden aus. Emit übernimmt als ersten Parameter den Namen des auszulösenden Events. Die folgenden Parameter reicht das Skript an die Callback-Funktion weiter.

Streams

Streams vereinfachen das Lesen und Schreiben von Daten in Node.js. Ein Stream bietet den Zugriff auf seine Daten mit Hilfe der gleichnamigen Schnittstelle. Eine HTTP-Anfrage – etwa »req« in Listing 2 – ist ein lesender Stream. Eine HTTP-Antwort – etwa »resp« in Listing 2 – ist ein schreibender Stream.

Ein lesender und ein schreibender Stream lassen sich mit der Methode »pipe()« verlustfrei synchronisieren. Pipe unterbricht die Synchronisation, wenn ein Buffer überzulaufen droht. Listing 5 zeigt Streams am Beispiel eines streamenden HTTP-Servers. Die Zeilen 1 bis 3 binden zunächst alle benötigten Module ein. Die Variable »www« in Zeile 4 speichert das Wurzelverzeichnis des HTTP-Servers, das Objekt »mimes« in Zeile 5 die Mime-Typen, das Objekt »errors« in Zeile 6 alle Statusmeldungen aus dem HTTP-Protokoll. Die Zeilen 8 bis 20 erzeugen das HTTP-Server-Objekt und binden es an den Port 3001.

Listing 5

Pipe synchronisiert

01 var http=require('http'),
02   path=require('path'),
03   fs=require('fs'),
04   www="www",
05   mimes={"jpg":"image/jpeg","webm":"video/webm"};
06   errors={404:"Not found", 415:"Unsupported Media Type"};
07
08 http.createServer(function(req, res) {
09  var med=path.join(www, req.url),
10   mime=mimes[path.extname(med).substring(1)];
11  if (mime) {
12   var rst=fs.createReadStream(med);
13   rst.on("open", function() {res.writeHead(200, {'Content-Type': mime})});
14   rst.on("end", function() {res.end();});
15   rst.on("error", function() {error(res, 404, med)});
16   rst.pipe(res);
17  } else {
18   error(res, 415, med);
19  }
20 }).listen(3001);
21
22 function error(res, code, path) {
23   res.writeHead(code, {'Content-Type': "text/html"});
24   res.end("<h1>"+code+" - "+errors[code])
25 }

Die Callback-Funktion findet sich in den Zeilen 8 bis 20. Zunächst hängt Zeile 9 den Pfadanteil der aufgerufenen URL an den Pfad des Wurzelverzeichnisses an und speichert ihn in der Variablen »med« . Die folgende Zeile liest den Mime-Type der HTTP-Antwort anhand der Datei-Erweiterung der angefragten Datei aus dem Objekt »mimes« aus und speichert ihn in der Variablen »mime« .

Für einen bekannten Mime-Typ erzeugt Zeile 12 einen lesenden Stream auf der abgefragten Datei »med« und speichert ihn im Objekt »rst« . Die Übertragung der Datei in die HTTP-Antwort mit »pipe()« in Zeile 16 beginnt erst nach dem Ausführen der Callback-Funktion zum Event »open« in Zeile 13. Den Abschluss der Übertragung macht die HTTP-Antwort im Callback zum Event »end« . Im Fehlerfall (Zeile 15 oder 18) gibt der Server statt der Datei eine Fehlermeldung aus (Funktion »error()« in den Zeilen 22 bis 25). Abbildung 3 zeigt ein Video im Webbrowser Firefox, das Node.js als HTTP-Server auf den Client streamt.

Abbildung 3: Live vom Localhost: Der HTTP-Server streamt ein Video.

Abbildung 3: Live vom Localhost: Der HTTP-Server streamt ein Video.

Module

Node.js eröffnet mit seinen Modulen die Möglichkeit, Objekte auszulagern und erneut zu verwenden. Listing 6 zeigt den Echo-Server aus Listing 2 als Node.js-Modul. Es liegt im aktuellen Arbeitsverzeichnis in der Datei »echo.js« . Der Quelltext des Echo-Servers aus Listing 2 kommt in die Methode »start()« des Objekts »exports« . Listing 7 schließlich zeigt, wie sich das Modul verwenden lässt, wenn es ebenfalls im aktuellen Arbeitsverzeichnis liegt.

Listing 6

Echo-Server als öffentliche Methode

01 var http = require('http');
02 exports.start = function(port) {
03  return http.createServer(function(req, resp) {
04   resp.writeHead(200, {'Content-Type': 'text/plain'});
05   resp.end(req.url);
06  }).listen(port);
07 }

Listing 7

Module unter Node.js

01 var echo = require('./echo.js');
02 var server = echo.start(3000);

Die Require-Funktion in Zeile 1 bindet das Modul über den relativen Pfad »./echo.js« ein. Der Aufruf von Require gibt eine Referenz auf das Objekt »exports« in Listing 6 zurück. Exports enthält alle öffentlichen Eigenschaften und Methoden des Moduls, auch die Methode »start()« aus Zeile 2.

Node.js-Module lassen sich zudem mit dem mitgelieferten Paketmanager Npm packen, veröffentlichen und wieder einbinden. Das Projekt Npmjs [5] verwaltet zurzeit etwa 20 000 gepackte Module. Mit »sudo npm install package-name« installiert Npm das gepackte Modul lokal im Unterverzeichnis »node_modules« des aktuellen Arbeitsverzeichnisses. »sudo install -g package-name« dagegen installiert es global in der Verzeichnisstruktur der Node.js-Distribution.

Listing 8 zeigt beispielhaft die Verzeichnisstruktur des entpackten Moduls »echo« . Es beinhaltet eine Paketbeschreibung in der Datei »package.json« sowie das eigentliche Echo-Modul aus Listing 6 in der Datei »index.js« . Die Paketbeschreibung in Listing 9 enthält ein Json-Objekt mit den Feldern »name« , »version« und »dependencies« , also Abhängigkeiten zu anderen Modulen.

Listing 8

Verzeichnisstruktur des Echo-Moduls

01 |+ node_modules
02  |+ echo
03   |- package.json
04   |- index.js

Listing 9

Paketbeschreibung

01 {
02   "name": "Echo Server",
03   "version": "0.0.1",
04   "dependencies": {
05   "http": "*"
06   }
07 }
Abbildung 4: Mit einem Klick auf »Checkout« meldet sich der Fahrer am Parkhaus ab.

Abbildung 4: Mit einem Klick auf »Checkout« meldet sich der Fahrer am Parkhaus ab.

Abbildung 5: Datenbanksysteme können durch geringere Konsistenz an Verfügbarkeit gewinnen.

Abbildung 5: Datenbanksysteme können durch geringere Konsistenz an Verfügbarkeit gewinnen.

Echo hängt lediglich von »http« ab. Der Feldwert »*« in Zeile 5 steht dabei für jede beliebige Version von »http« . Alternativ kann der Admin hier auch – wie in Listing 13 – konkrete Versionsnummern angeben. Module im Unterverzeichnis »node_modules« findet die Require-Funktion anhand des Verzeichnisnamens, beispielsweise »require(‘echo’)« .

Npm bringt für Node.js zahlreiche Erweiterungen, um funktionale Webanwendungen in wenigen Schritten zu erstellen. Das folgende Beispiel zeigt, wie Programmierer mit Node.js, dem Webframework Express, der Templatesprache Jade [6] und dem NoSQL-Datenbanksystem Redis [7] eine Webanwendung bauen. Abbildung 4 zeigt als Beispielanwendung ein Buchungssystem für Parkhäuser im Einsatz. Mit Buchungscode kann der Fahrer einchecken, ohne zu warten. Nach dem Checkout gibt das System den Parkplatz wieder zur Reservierung frei.

Redis

Relationale Datenbanksysteme wie etwa MySQL erwarten, dass der Programmierer alle Datenbankobjekte deklariert hat. Die konstante Pflege des Schemas wird schnell aufwändig und damit kostenintensiv. Zwar speichert ein relationales Datenbanksystem die Daten stets konsistent, doch die dazu nötigen Maßnahmen gehen auch zu Lasten der Verfügbarkeit. NoSQL-Datenbanksysteme wie Redis verringern die Konsistenz der Daten zugunsten höherer Verfügbarkeit (Abbildung 5). Eine vorherige Deklaration der Objekte ist nicht nötig.

Im folgenden Beispiel soll Redis die Daten der Anwendung speichern. Redis ist Open-Source-Software unter BSD-Lizenz und lag bei Redaktionsschluss in Version 2.6.5 vor. Unter Ubuntu 12.10 installiert der Admin Redis mit dem simplen Befehl »sudo apt-get install redis« .

Redis speichert strukturierte Daten wie Listen, Hashes, Mengen und Zeichenketten im Hauptspeicher des Servers und auf Wunsch auch auf der Festplatte. Die Datenhaltung im Hauptspeicher macht Redis performant: Obwohl der Redis-Server nur in einem Prozess läuft, kann er auf einem durchschnittlichen Linux-Server bis zu 500000 Operationen pro Sekunde ausführen [8].

Die Leistung der NoSQL-Datenbank ergibt sich primär durch den freien Hauptspeicher, es lassen sich aber auch mehrere Server zu einem Cluster zusammenfassen. Zudem bietet Redis atomare Operationen und verfügt über einen Transaktions-Mechanismus. Listing 10 zeigt Redis in der Beispielanwendung zusammen mit Node.js.

Listing 10

Redis anbinden

01 var db = require('redis').createClient();
02
03 exports.book = function(req, res) {
04  stats(function(free) {
05   if (free > 0) {
06     var code = "XCP"+Math.floor(Math.random()*1000);
07     db.sadd("open", code);
08   }
09   show(res, 'book', (free>0)?0:1, code);
10  });
11 };
12
13 exports.checkin = function(req, res) {
14   var code = req.body.code;
15  db.srem("open", code, function(err, rp) {
16   if (rp > 0) {
17     db.sadd("checkedIn", code);
18   }
19   show(res, 'checkin', (rp>0)?0:1, code);
20  });
21 };
22
23 exports.checkout = function(req, res) {
24   var code = req.body.code; // closure
25   db.srem("checkedIn", code, function(err, rp) {
26   show(res, 'checkout', (rp>0)?0:1, code);
27  });
28 };

Redis anbinden

In Zeile 1 bindet die Require-Funktion zunächst das Modul »redis« ein. Anschließend erstellt die Methode »createClient()« eine Verbindung zum Redis-Server auf dem lokalen Webserver und speichert diese in der Variablen »db« . Die Methode »book()« bindet in Zeile 3 das Anlegen einer Buchung an die Auslastung des Parkhauses. Ist es nicht ausgebucht, dann erzeugt Zeile 6 einen neuen Buchungscode (Variable »code« ). In der folgenden Zeile fügt das Skript den Buchungscode mit der Methode »sadd()« in Redis der Menge der unbestätigten Buchungen (Schlüssel »open« ) hinzu.

Sadd und Srem

Die Methode »sadd()« übernimmt im ersten Parameter den Schlüsselnamen und im zweiten den Wert, dem die unbestätigten Buchungen zugeschlagen werden sollen. Am Präfix »s« von »sadd()« lässt sich der Datentyp »set« ablesen. Falls »open« in der Datenbank noch nicht existiert, legt die Datenbank ihn vor dem Hinzufügen des Buchungscodes mit »sadd()« an. Die Methode »checkin()« entfernt in Zeile 15 mit »srem()« dagegen den übergebenen Buchungscode (Variable »code« ) aus der Menge der unbestätigten Buchungen in Redis. »srem()« übernimmt im ersten Parameter den Schlüsselnamen, im zweiten den zu entfernenden Wert und im dritten eine Callback-Funktion.

Wie alle Methoden des Node.js-Moduls für Redis [9] führt dieses auch die Methode »srem()« asynchron aus und ruft nach dem Entfernen des Werts aus dem Set die Callback-Funktion aus. Diese übernimmt im ersten Parameter eine mögliche Fehlermeldung und im zweiten den Rückgabewert der Redis-Operation. Die Callback-Funktion in Zeile 15 fügt den Buchungscode mit der Methode »sadd()« der Menge der bestätigten Buchungen zum Schlüssel »checkedIn« in Redis hinzu. Die Methode »checkout()« nutzt in Zeile 25 wiederum »srem()« , um einen Buchungscode nach dem Checkout endgültig zu entfernen. Damit steht der Parkplatz neu ankommenden Autofahrern wieder zur Verfügung.

Redis-Transaktionen

Die Funktion »stats()« in Listing 11 zeigt beispielhaft, wie Programmierer Rdis-Transaktionen verwenden. Zunächst öffnet Zeile 2 mit der Methode »multi()« eine Transaktion und speichert sie in der gleichnamigen Variablen ab. Die Methoden »get()« und »scard()« – auf das Transaktionsobjekt »multi« angewendet – lassen die Operationen innerhalb der Transaktion ablaufen. Dies kann auch – wie in Zeile 3 – als Verkettung geschehen. Die Methode »exec()« in Zeile 4 schließt die Transaktion ab.

Listing 11

Zusammengefasste Transaktionen

01 function stats(cbk) {
02  var multi = db.multi();
03  multi.get('max').scard('open').scard('checkedIn');
04  multi.exec(function(err, rp) {
05   cbk((rp[0] - rp[1] - rp[2]), rp[1], rp[2]);
06  });

Die Parameterliste der übergebenen Callback-Funktion ist wie in Listing 10 strukturiert. Jedoch speichert Listing 11 die Rückgabewerte der Redis-Operationen in einem Feld. Die vorliegende Callback-Funktion ruft eine weitere Callback-Funktion auf, die im ersten Parameter die Anzahl der freien Parkplätze, im zweiten die Anzahl der unbestätigten Buchungen und im dritten die der bestätigten Buchungen übernimmt. Die Methode »scard()« zählt die Mitglieder einer Menge.

Express und Connect

Das Webframework Express, mit dem im Folgenden die Beispielanwendung erstellt wird, ist Teil des Middleware-Frameworks Connect [10]. Das unter MIT-Lizenz erscheinende Connect liegt derzeit in der Version 2.7.1 vor. Npm installiert es lokal mit dem Befehl »sudo npm install connect« . Connect dient zum Einbinden von Middleware, also von Programmen, die Standardaufgaben wie das Protokollieren oder das Parsen von HTTP-Anfragen oder die Verwaltung einer Session übernehmen.

Connect enthält bereits über 20 verschiedene Middleware-Funktionen. Weitere Middleware findet sich im Wiki von Connect [11]. Listing 12 zeigt den Echo-Server mit Connect. Die Methode »use()« bindet in Zeile 3 die Middleware »logger« ein. Diese protokolliert HTTP-Anfragen per Standardausgabe. Zeilen 4 bis 6 beantworteten die HTTP-Anfrage im Stil des Echo-Servers aus Listing 2. Abbildung 6 zeigt die Ausgabe des Loggers.

Listing 12

use() bindet Middleware ein

01 var connect = require('connect');
02 var app = connect()
03  .use(connect.logger())
04  .use(function(req, res) {
05   res.end(req.url)
06  })
07  .listen(3000);

Listing 13

project.js

01 {
02   "name": "Buchung",
03   "version": "0.0.1",
04   "dependencies": {
05     "express": "3.0.4",
06     "jade": "*",
07     "redis": "*"
08   }
09 }
Abbildung 6: Die Middleware Logger protokolliert die HTTP-Anfragen per Standardausgabe.

Abbildung 6: Die Middleware Logger protokolliert die HTTP-Anfragen per Standardausgabe.

Das Webframework Express [12] erleichtert das Entwickeln von Webanwendungen unter Node.js erheblich. Neben der Einbindung von Middleware per Connect bietet Express zudem Routen, verschiedene Template-Engines sowie viele hilfreiche API-Funktionen. Express erscheint unter MIT-Lizenz und weist derzeit die Versionsnummer 3.0.5 auf.

Npm installiert es global mit dem Befehl »npm install -g express« . Danach schafft sich der Programmierer mit dem gleichnamigen Kommandozeilentool ein passendes Gerüst, beispielsweise »express booking« . Das landet dann im aktuellen Arbeitsverzeichnis, die angelegten Dateien und Verzeichnisse sind in Abbildung 7 dargestellt.

Abbildung 7: Das Kommandozeilentool Express beim Anlegen eines Anwendungsgerüsts.

Abbildung 7: Das Kommandozeilentool Express beim Anlegen eines Anwendungsgerüsts.

Mit Jade und Redis

Listing 13 zeigt die zugehörige Paketbeschreibung »project.js« . Das Beispiel hängt von Express, Redis und der Template-Engine Jade ab. Die abschließende Befehlsfolge »cd booking && npm install« installiert alle benötigten Module im Unterverzeichnis »node-modules« .

In Listing 14 findet sich der Quellcode der Beispielanwendung aus der Datei »app.js« . Die Zeilen 1 bis 4 binden die benötigten Module ein, Zeile 5 erzeugt das Express-Objekt »app« . Die Zeilen 7 bis 8 machen Angaben zur verwendeten Template-Engine Jade. Die Methode »use()« in den Zeilen 9 bis 18 bindet die Middleware ein.

Listing 14

Kern der Beispielanwendung

01 var express = require('express'),
02   routes = require('./routes'),
03   http = require('http'),
04   redis = require('redis'),
05   app = express();
06
07 app.set('views', __dirname + '/views');
08 app.set('view engine', 'jade');
09 app.use(express.static(__dirname + '/public'));
10 app.use(express.bodyParser());
11 app.use(function(req, res, next) {
12   redis.createClient().set('max', 10, function() {next();});
13 });
14 app.use(app.router);
15 app.use(function(err, req, res, next) {
16   console.log(err.stack);
17   res.send(500, '<h1>Internal Server Error</h1>');
18 });
19
20 app.get('/', routes.index);
21 app.post('/book', routes.book);
22 app.post('/checkin', routes.checkin);
23 app.post('/checkout', routes.checkout);
24 app.post('/reset', routes.reset);
25 app.listen(3003);

Listing 15

Verweis auf eine CSS-Datei

01 doctype 5
02 html
03  head
04   link(rel='stylesheet', href='/stylesheets/style.css')
05  body
06   #ticket
07     #symbol P
08     block content

Dabei ist die Reihenfolge des Aufrufs der Middleware wichtig: Die einzelnen Komponenten werden für alle HTTP-Anfragen nacheinander aufgerufen. Findet beispielsweise die Middleware aus Zeile 9 anhand der URL ein gleichnamiges Dokument im Verzeichnis »public« , so sendet sie dies und schließt das Bearbeiten der HTTP-Anfrage ab.

Das Parsen des Körpers der HTTP-Anfrage in Zeile 10 erfolgt nur für HTTP-Anfragen, zu deren URL Zeile 9 kein passendes Dokument gefunden hat. Zeile 12 speichert die Anzahl der Parkplätze zum Schlüssel »max« in Redis. Der Aufruf der Funktion »next()« ohne Wert (Abbildung 8) in der Callback-Funktion startet die nächste Middleware. Zeile 14 aktiviert die Behandlung von Routen.

Abbildung 8: Der Aufruf von »next()« ohne Wert geht zur nächsten Middleware über.

Abbildung 8: Der Aufruf von »next()« ohne Wert geht zur nächsten Middleware über.

Die Zeilen 15 bis 18 legen eine Callback-Funktion für Fehler fest. Diese Funktion verwendet vier Parameter: Der erste übernimmt eine Fehlermeldung, der zweite die HTTP-Anfrage, der dritte die HTTP-Antwort, der vierte eine Referenz auf die Funktion »next()« . Die Callback-Funktion lässt sich mit »next()« und einem nicht leeren Parameter aufrufen, zum Beispiel mit »next(‘user error’)« .

Die Methoden-Aufrufe in den Zeilen 20 bis 24 weisen dem Pfadanteil der aufgerufenen URL eine Callback-Funktion aus dem Objekt »routes« zu. Die Methoden-Namen korrespondieren mit der Methode der HTTP-Anfrage. Nur wenn Pfadanteil und Methode übereinstimmen, wird die Callback-Funktion aufgerufen. Mit »all()« statt »get()« oder »post()« lässt sich jede Methode erfassen. Die Routenbeschreibungen können auch reguläre Ausdrücke enthalten, beispielsweise passt »/\/\w +\/node/« auf die URLs »/v8/code« oder »/js/node« . Die Zeichenkette »’*’« steht für die Defaultroute.

Listing 16 zeigt die Callback-Funktionen für die Routen aus Listing 14, die in Listing 10 noch nicht enthalten waren. Die Funktion »index()« erzeugt in Zeile 2 durch Aufruf der in Zeile 10 implementierten Funktion »show()« die Startseite der Beispielanwendung. »show()« übernimmt im ersten Parameter das Objekt der HTTP-Antwort und im zweiten den Namen einer Jade-Vorlage. Show benutzt die in Listing 11 beschriebene Funktion »stats()« , um die Erzeugung des HTML-Dokuments aus dem Jade-Template mit den statistischen Daten des Parkhauses zu parametrisieren.

Listing 16

Exportieren

01 exports.index = function(req, res){
02   show(res, 'index');
03 };
04
05 exports.reset = function(req, res) {
06   db.del('open', 'checkedIn');
07   res.render('reset', par());
08 }
09
10 function show(res, tpl, err, code) {
11  stats(function(free, open, ckd) {
12   res.render(tpl, par(free, open, ckd, err, code));
13  });
14 }
15
16 function par(free, open, ckd, err, code) {
17   return {"code": code, "err": err, "free": free, "open": open, "ckd": ckd};
18 }

Die Methode »reset« in Zeile 5 löscht die Sets »open« und »checkedin« in Redis, bevor sie mit »render()« eine Bestätigung in HTML ausgibt. Die Methode »render()« wiederum erzeugt aus der Jade-Vorlage »reset« ein HTML-Dokument. Die Hilfsfunktion »par()« in Zeile 16 formatiert die statistischen Angaben zum Parkhaus zwecks Parametrisierung der Vorlage. Die Funktionen aus Listing 16 komplettieren also Listing 14. Der Quellcode ist in der Datei »routes/index.js« zu finden.

Jade-Vorlagen

Die Beispielanwendung verwendet Jade, um HTML-Dokumente zu erzeugen. Express speichert die Vorlagen in Dateien mit der Endung ».jade« im Unterverzeichnis »views« ab. Jade-Vorlagen sind in einer eigenen Sprache verfasst, die neben einer verkürzten Schreibweise von HTML-Markup auch Variablen, Schleifen und Verzweigungen umfasst.

Listing 17 zeigt eine Jade-Vorlage aus der Beispielanwendung. In Zeile 1 bindet die Direktive »extends« den Kopf des HTML-Dokuments aus der Vorlage »layout.jade« (siehe Listing 15) ein. Im Gegensatz zu einer einfachen Inklusion stellt sich die Vorlage durch »extends« als Erweiterung von »layout« dar.

Listing 17

Check-in am Parkhaus

01 extends layout
02
03 block content
04  #app
05   h3#free #{free} frei, #{open} gebucht und #{ckd} belegt
06   case err
07    when 1
08     h1 Checkin nicht möglich
09    default
10     h1 Sie sind eingecheckt
11     form(action="/checkout", method="post")
12      input(type="hidden", name="code", value=code)
13      input(type="submit", value="Checkout")
14   form(action="/", method="get")
15    input(type="submit", value="Erneut Buchen")
16   form(action="/reset", method="post")
17    input(type="submit", value="Zurücksetzen")

Ab Zeile 3 beginnt ein »content« -Bereich, die darauf folgende Zeile erzeugt einen »div« -Container mit dem Attributwert »app« für das Attribut »id« . Zeile 5 dereferenziert in dem »h3« -Element »free« die Werte der Variablen »free« , »open« und »ckd« ähnlich wie in Bash-Skripten.

Die Case-Anweisung ab Zeile 6 behandelt den Fehler in der Variablen »err« . Die Syntax von Jade vermeidet schließende Tags, Verschachtelungen zeichnet sie durch Einrückungen aus. Die Auszeichnung von Geschwister-Elementen beginnt stets in derselben Spalte, so wie in den Zeilen 10, 11, 12 oder 13.

Fazit

Die Stärken von Javascript lassen sich mit Node.js auch Server-seitig nutzen. Vielfältige Module wie Connect oder Express erweitern den Funktionsumfang des Node.js-Frameworks beträchtlich und erlauben effiziente Programmierung von performanten und gut skalierbaren Webanwendungen. Aber die Programmierung lässt sich auch auf die Client-Seite ausdehnen. So entstehen Webanwendung aus einem Guss statt eines Sprachengewirrs wie mit LAMP-Servern.

Der Autor

Dipl.-Phys. Andreas Möller http://pamoller.com beschäftigt sich seit 2001 mit der Entwicklung Internet-basierter Software. Dazu zählen Datenbank- und Webanwendungen sowie Arbeiten auf dem Gebiet des Single Source Publishing. Zurzeit ist er als Berater und freier Autor tätig.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 7 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben