Zeitgemäße Webanwendungen in JavaScript entwickeln

Node.js ist ein Framework zur Entwicklung serverseitiger Webanwendungen in JavaScript, das sich besonders gut für skalierbare, hochperformante und echtzeitfähige Webanwendungen eignet. Dieser Artikel erklärt, was Node.js auszeichnet, und erläutert die grundlegenden Konzepte.

In Pocket speichern vorlesen Druckansicht 8 Kommentare lesen
Lesezeit: 15 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Node.js ist ein Framework zur Entwicklung serverseitiger Webanwendungen in JavaScript, das sich besonders gut für skalierbare, hochperformante und echtzeitfähige Webanwendungen eignet. Zudem ermöglicht es Webentwicklern die einheitliche Verwendung von JavaScript für Client und Server. Dieser Artikel erklärt, was Node.js auszeichnet, und erläutert die grundlegenden Konzepte.

Der Begriff Node.js geistert seit 2009 durch die IT-Fachpresse und wird mit einer gewissen Regelmäßigkeit als "das nächste große Ding" bezeichnet. Erfinder von Node.js ist Ryan Dahl, der das Framework 2009 erstmals veröffentlicht hat. Einen besonderen Schwerpunkt legt Node.js auf die einfache und zugleich hohe Skalierbarkeit von Webanwendungen, um dem ständig wachsenden Bedarf an zahlreichen gleichzeitigen und dauerhaften Verbindungen gerecht zu werden.

Da Node.js Googles schnellen JavaScript-Compiler V8 enthält, verfügen die entwickelten Webanwendungen über eine hohe Performance. Der funktionale Ansatz von JavaScript ermöglicht darüber hinaus eine gute Parallelisierbarkeit der Ausführung. Zudem können auch echtzeitfähige Webanwendungen entwickelt werden, da WebSockets und Streaming fundamentale Konzepte in Node.js sind. Mit wenigen Zeilen JavaScript-Code lassen sich deshalb leistungsfähige und zeitgemäße Webanwendungen erstellen, die Desktop-Programmen nicht nachstehen.

Entwicklung und Ausführung erfolgen dabei plattformunabhängig: Node.js steht ohne zeitaufwendige Installation für Linux, Mac OS X und Windows zur Verfügung und lässt sich mit den gängigen Texteditoren und zahlreichen integrierten Entwicklungsumgebungen verwenden.

Unterstützt wird Node.js von der Firma Joyent und einer engagierten Community. Diese hat innerhalb von zweieinhalb Jahren ein umfassendes Ökosystem mit etwa 15.000 Komponenten für nahezu jeden Anwendungsfall geschaffen und arbeitet stetig an weiteren Erweiterungen und Ergänzungen.

Mit der zunehmenden Verbreitung von Breitbandtechniken, mobilen Endgeräten und der zeitnahen Bereitstellung von Daten steigen die Ansprüche, die Anwender an Webanwendungen stellen. Fasst man diese in Kategorien zusammen, ergeben sich drei grundlegende Forderungen, die zeitgemäße Implementierungen von den Vertretern aus den Zeiten des Web 1.0 und 2.0 unterscheiden:

  • Moderne Webanwendungen müssen weitaus besser skalieren, um das C10K-Problem lösen und 10.000 Clients gleichzeitig bedienen zu können.
  • Sie müssen eine hohe Performance aufweisen, sodass sie einer nativen Anwendung weder in Komfort noch in Reaktivität nachstehen.
  • Zeitgemäße Webanwendungen müssen im Idealfall echtzeitfähig sein, um den Benutzer so schnell wie möglich mit neuen Informationen zu versorgen.

An diesen Punkten setzt Node.js an.

Node.js löst das C10K-Problem, indem es einen anderen Ansatz verfolgt als klassische Webserver: Statt für jede eingehende Anfrage einen eigenen Thread zu verwenden, verarbeitet das Framework sämtliche Anfragen eines Threads nacheinander. Damit der nicht blockiert, muss er seine Arbeit nach Möglichkeit delegieren.

Hierbei kommt Node.js zugute, dass die meisten Webanwendungen Abhängigkeiten auf externe Ressourcen wie das Dateisystem oder eine Datenbank aufweisen und die Anfragen an diese Ressourcen zeitaufwendig sind. Im Gegensatz zu einem klassischen Webserver, dessen Threads die meiste Zeit mit Warten auf die Beendigung einer I/O-Operation beschäftigt sind, verwendet Node.js sogenanntes asynchrones oder nichtblockierendes I/O.

Asynchrones I/O bedeutet, dass der Node.js-Thread eine eingehende Anfrage bis zur ersten Interaktion mit einer externen Ressource verarbeitet, dann diese Interaktion startet und die Anfrage so lange beiseite legt, bis eine Antwort von der Ressource vorliegt.

In der Zwischenzeit kümmert sich Node.js um die nächste Anfrage und verarbeitet diese bis zur Interaktion mit einer externen Ressource und so weiter. Sobald die Antwort vorliegt, wird ein Callback ausgeführt und die Anfrage fortgesetzt: entweder bis zur nächsten Interaktion mit einer externen Ressource oder bis zu deren Abschluss. Auf diese Art kann Node.js mit einer weitaus höheren Anzahl von Anfragen umgehen, als das für einen klassischen Webserver je möglich wäre.

Da Node.js sämtlichen Code per se asynchron ausführt, ist bedeutend weniger Aufwand in die Nebenläufigkeit einer Anwendung zu investieren, was die Entwicklung in Node.js auch für Programmierer mit wenig Erfahrung in der Parallelisierung verhältnismäßig vereinfacht. Node.js selbst stellt jedoch keine vollständige Laufzeitumgebung dar, sondern lediglich ein Framework für asynchrones I/O, das eine Klassenbibliothek zur Entwicklung serverseitiger Webanwendungen ergänzt.

Als Laufzeitumgebung dient V8 , die somit für den Entwickler de facto transparent ist. Deren Verwendung trägt einen großen Anteil zur Ausführungsgeschwindigkeit von Node.js bei, was in Verbindung mit dem zuvor genannten Modell zur Skalierbarkeit eine performante Umgebung ergibt.

Node.js schätzt zudem das HTTP-Protokoll, weshalb es dieses nicht durch unnötige Abstraktionsschichten vor dem Entwickler zu verbergen versucht. Dadurch wird zum einen eine direkte Art der Entwicklung möglich – eine Herangehensweise, die Webentwickler durchaus schätzen. Schließlich achtet das Web selbst die gleiche Kultur: Einfache, voneinander unabhängige Bausteine genügen, wobei die eigentliche Macht aus der flexiblen Kombination der Einzelteile entsteht. Zum anderen ermöglicht Node.js damit auch den direkten Zugriff auf den ein- und ausgehenden Datenstrom, sogar während der Datenübertragung, was es beispielsweise für Streaming prädestiniert.

Auch andere Echtzeitszenarien lassen sich in Node.js umsetzen: Die Unterstützung von WebSockets ermöglicht beispielsweise Anwendungen, bei denen ein Webserver nach der abgeschlossenen Übertragung einer Webseite an den Client noch Informationen im Push-Verfahren senden kann. Für all das wird eine Programmiersprache genutzt, die vielen Webentwickler vertraut ist: JavaScript. Für Node.js entwickelte Webanwendungen sind in JavaScript geschrieben. Im Gegensatz zu klassischem JavaScript findet die Ausführung nun allerdings auf dem Server und nicht auf dem Client statt.

JavaScript-Code lässt sich mit Node.js außer auf dem Client nun auch auf dem Server verwenden. Insbesondere Code, den ohnehin beide Instanzen ausführen, drängt sich dafür auf. Das Paradebeispiel hierfür stellt Validierungslogik dar, die aus Komfortgründen zunächst auf dem Client, aus Sicherheitsgründen aber auch auf dem Server auszuführen ist. Diese ist bei Verwendung von Node.js nur einmal zu implementieren und lässt sich dann auf beiden Seiten nutzen.

Nicht nur das HTTP-Protokoll ist in Node.js ein Bürger erster Klasse, auch das Datenformat JSON (JavaScript Object Notation) zählt als solcher. Das bedeutet, dass sich Node.js besonders zur Implementierung von mit JSON arbeitenden APIs oder Webdiensten eignet, die dem REST-Paradigma (Representational State Transfer) folgen. Selbiges gilt für Webanwendungen, die lediglich aus einer einzigen HTML-Seite bestehen und ihre weitere Logik in JavaScript abbilden.

Als Faustregel zur Benutzung von Node.js können die genannten Kriterien Skalierbarkeit, Performance und Echtzeitfähigkeit gelten: Sobald eine dieser Eigenschaften unverzichtbar für den Erfolg einer Webanwendung ist, stellt es höchstwahrscheinlich eine geeignete Plattform für deren Implementierung dar.

In einigen Fällen ist der Einsatz von Node.js allerdings ungeeignet oder zumindest wenig nützlich: Findet kaum Interaktion mit externen Ressourcen statt, wird der Thread unnötig blockiert, und die eingehenden Anfragen müssen eventuell lange warten. Das Verhalten gestaltet sich je schlimmer, desto rechenintensiver eine Webanwendung ist. Typische CRUD-Anwendungen (Create, Read, Update, Delete), die Daten ohne größere Verarbeitung lediglich zwischen Webbrowser und Datenbank übertragen, sind ebenfalls ungeeignet.

Um Node.js überhaupt ausführen zu können, stehen prinzipiell zwei Möglichkeiten zur Verfügung: Zum einen lässt es sich, wie zumindest unter Linux üblich, aus dem Quelltext übersetzen. Zum anderen kann ein auf das jeweilige Betriebssystem angepasstes vorgefertigtes Installationspaket verwendet werden. Auf der Webseite von Node.js finden sich mittlerweile Pakete für Mac OS X und Windows, für Linux stehen vorübersetzte Binärpakete zur Verfügung.

Nach der Installation von Node.js besteht der einfachste Weg zu überprüfen, ob die Installation erfolgreich war, darin, die Anwendung node über eine Konsole aufzurufen:

$ node

Erscheint daraufhin ein >-Zeichen als Eingabeaufforderung, hat es funktioniert. Nun ist die Eingabe von JavaScript-Anweisungen möglich, die direkt übersetzt und ausgeführt werden.

Um Node.js wieder zu beenden, genügt ein Tastendruck auf Strg + C. Um sicherzustellen, dass diese Tastenkombination nicht versehentlich eingegeben wurde und die aktive Sitzung damit unbeabsichtigt beendet, erfolgt eine zweite Eingabeaufforderung.

Um die Fähigkeiten von Node.js über die Grundlagen von JavaScript hinaus zu erweitern, lassen sich Module ergänzen. Neben der Möglichkeit, diese aus dem Web herunterzuladen und nachträglich zu installieren, enthält schon die Standardinstallation einige integrierte Komponenten.

Das für die Webentwicklung wichtigste ist das http-Modul, das Funktionen zum Implementieren von Webservern und -clients enthält. Um es verwenden zu können, ist es zunächst mit der require-Funktion in die eigene Anwendung zu importieren:

var http = require('http');

Die Zuweisung an die Variable http ermöglicht dabei den späteren Zugriff auf das Modul, um beispielsweise dessen Objekte und Funktionen verwenden zu können.

Das Modul http enthält die Funktion createServer, mit deren Hilfe sich ein HTTP-Server erzeugen lässt. Dieser Funktion wird eine weitere als Callback übergeben, die für die Verarbeitung von eingehenden HTTP-Anfragen und deren Beantwortung zuständig ist. Der Zugriff auf die zugrunde liegenden Anfrage und die dazugehörige Antwort erfolgt dabei über die beiden Parameter req und res:

var server = http.createServer(function (req, res) {
// ...
});

Analog zur Konsolen- soll auch die Webanwendung zunächst den Text Hallo Welt! zurückgeben. Als Zeichen, dass die Verarbeitung der Anfrage erfolgreich war, werden zudem der HTTP-Statuscode 200 gesendet und der MIME-Type der Antwort auf text/plain gesetzt.

Beides erfolgt mit einem Aufruf der Funktion writeHead des Objekts res, wobei sich zusätzlich zum MIME-Type auch beliebige andere Header-Informationen senden lassen, indem sie dem Parameterobjekt hinzugefügt werden:

res.writeHead(200, {
'content-type': 'text/plain'
});

Um die eigentliche Ausgabe vorzunehmen, dient die Funktion write. Nach dem Senden ist die Antwort des Webservers mit der Funktion end abzuschließen .

res.write('Hallo Welt!\n');
res.end();

Zur Ausführung muss der Webserver noch an einen Port gebunden werden, der dafür an die Funktion listen übergeben wird.

server.listen(3000);

Alternativ lässt sich diese Funktion, im Sinne eines flüssigeren Schreibstils, auch direkt nach dem Aufruf von createServer aufrufen:

http.createServer(function (req, res) {
// ...
}).listen(3000);

Nachdem der gesamte Code in eine Datei, in diesem Beispiel app.js, gespeichert wurde, ist der Webserver aufrufbereit:

$ node app.js

Obwohl Node.js bereits einige Module enthält, gibt es zahlreiche Anwendungsfälle, die davon nicht abgedeckt sind. Dazu zählen beispielsweise das Internationalisieren und Lokalisieren von Anwendungen, der Zugriff auf Datenbanken oder das Debuggen mit einer grafischen Benutzeroberfläche.

Für viele dieser Belange existieren Implementierungen, die die Entwickler der Community in der Regel kostenfrei zur Verfügung stellen. Für die Integration der Module in eigene Anwendungen, und zur Verwaltung der Abhängigkeiten zwischen ihnen dient der Node.js Package Manager (npm). Seit Node.js 0.6.3 entfällt die gesonderte Installation von npm, da er bereits in der Standardinstallation enthalten ist.

Um ein Modul zu installieren, ist npm außer dem Parameter install auch dessen Name zu übergeben:

$ npm install node-force-domain
node-force-domain@0.0.4 ./node_modules/node-force-domain

Soll nicht die aktuelle, sondern eine vorherige Version einer Komponente installiert werden, lässt sich die Versionsnummer bei der Installation gezielt angeben:

$ npm install node-force-domain@0.0.3
node-force-domain@0.0.3 ./node_modules/node-force-domain

Bei erfolgreicher Installation meldet npm die Version des installierten Moduls und den zugehörigen Pfad. In beiden Fällen steht es der Anwendung danach zur Verfügung und lässt sich mit der require-Funktion importieren:

var forceDomain = require('node-force-domain');

Sofern ein Modul von weiteren abhängt, löst npm diese Abhängigkeiten automatisch auf und installiert auch die anderen, wobei er diese in einem speziellen Unterordner ablegt. Das stellt sicher, dass verschiedene Module von unterschiedlichen Versionen ein- und desselben Moduls abhängen können, ohne dass es zu Versionskonflikten kommt.

Sollen die Abhängigkeiten einer Anwendung festgeschrieben werden, lässt sich zu diesem Zweck package.json verwenden. npm kann damit sämtliche Abhängigkeiten automatisch auflösen.
Dazu ist zunächst eine Datei package.json zu erzeugen und im Wurzelverzeichnis der Anwendung zu hinterlegen. Prinzipiell muss sie mindestens zwei Elemente enthalten: den Namen der Anwendung und deren Versionsnummer. Beide Angaben sind zwingend erforderlich:

{
"name": "myapp",
"version": "0.0.1"
}

Um nun die Abhängigkeiten einer Anwendung zu anderen Modulen anzugeben, ist package.json um eine Eigenschaft dependencies zu ergänzen, wobei deren Wert einem Objekt entspricht, das die folgenden Beziehungen definiert:

{
"name": "myapp",
"version": "0.0.1",
"dependencies": {
"node-force-domain": "0.0.4"
}
}

Nach der Definition der Zusammenhänge einer Anwendung in der Datei package.json kann npm diese automatisch auflösen. Deshalb ist es nicht erforderlich, das node_modules-Verzeichnis in die Versionsverwaltung einzubinden. Stattdessen genügt es, die eigentliche Anwendung und die Datei package.json zu speichern. Sämtliche Abhängigkeiten lassen sich nun durch Aufrufen von npm mit dem Parameter install ohne Angabe eines Moduls analysieren und auflösen:

$ npm install
node-force-domain@0.0.4 ./node_modules/node-force-domain

Mit diesem Wissen ist der Grundstein gelegt, um eigene Programme mit Node.js zu entwickeln. Auffällig ist die Einfachheit, mit der sich HTTP-Anwendungen implementieren lassen. Das ist letztlich der Tatsache geschuldet, dass die Schnittstelle von der Außenwelt zur http-Funktion einfach gehalten ist: Es gibt nur eine einzige Funktion, die den Zugriff auf die eingehende Anfrage und die ausgehende Antwort ermöglicht. Jede diese Funktion bedienende Anwendung kann Node.js im Web ausführen.

Mehr Infos

Node.js & Co.

Dieser Artikel beruht auf Auszügen des Buches "Node.js & Co.", das im September 2012 im dpunkt.verlag erschienen ist und in die iX Edition aufgenommen wurde.

Der Autor führt den Leser darin Schritt für Schritt in die Welt von Node.js ein. Er vermittelt ihm nötige Techniken, Kenntnisse und Fähigkeiten und bietet darüber hinaus einen Leitfaden durch das umfangreiche Ökosystem von Node.js, der helfen soll, auch komplexe Anwendungen zügig zu entwickeln.

Natürlich ist es reizvoll und gelegentlich auch erforderlich, auf diesem niedrigen Abstraktionsniveau zu arbeiten, im Regelfall wird sich allerdings kaum ein Entwickler die Zeit nehmen können, ein eigenes Framework für Standardaufgaben wie Routing oder Templating zu erstellen.

Genau an der Stelle betritt das Ökosystem von Node.js die Bühne: Dort gibt es für zahlreiche Aufgabe ein oder mehrere Module. Das erleichtert und beschleunigt die Entwicklung ungemein – sofern man sich in der Vielfalt des Angebots zurechtfindet.

Node.js eignet sich besonders gut für skalierbare, hochperformante und echtzeitfähige Webanwendungen, die sich teilweise in wenigen Zeilen implementieren lassen. Die Vielfalt der Module mag zunächst verwirren, beweist jedoch auch, dass Node.js in weniger als drei Jahren einen Stand erreicht hat, der als Grundlage für umfangreiche Entwicklungen dienen kann. Nicht zuletzt aufgrund der großen Community und den bisherigen Erfolgsgeschichten ist davon auszugehen, dass Node.js Entwickler noch einige Zeit begleiten wird.

Golo Roden
ist freiberuflicher Wissensvermittler und Technologieberater für Webentwicklung, Codequalität und agile Methoden.

  • Golo Roden; Berechtigte Chancen für Node.js als "nächstes großes Ding", Artikel bei heise Developer