LIMESODA Blog

Node.js Basics und ein Vergleich mit PHP

PHP wird sich noch sehr lange auf den Servern und am Arbeitsmarkt halten, dennoch würde ich Entwickler-n00bs anraten ihre Lernzeit gleich für Node.js zu verwenden – oder statt Programmieren etwas gescheites zu lernen :). Der moderne Entwickler beherrscht den „Full Stack“, anstatt sich auf Backend oder Frontend zu konzentrieren. D.h. er hat vom Datenbankmodell bis zum User Interface alles im Griff. Mit Node.js ist es kein Problem mehr alle Teile einer Applikation gut zu beherrschen, die Programmiersprache ist JavaScript und Code kann – sofern sinnvoll umgesetzt – gleichzeitig auf Server und Client verwendet werden.

Atwood’s Law:

Any application that can be written in JavaScript, will eventually be written in JavaScript.

Das Gesetz hat sich mittlerweile mehrfach bewahrheitet, auch beim IoT (Internet of Things) rückt JavaScript immer mehr in den Vordergrund:

Installation

Node.js lässt sich sehr einfach installieren. Installationspaket downloaden und starten – eine Minute später ist Node.js einsatzbereit: https://nodejs.org/download/

Unter Linux gibt es mehrere Möglichkeiten, entweder man nutzt apt-get und bekommt je nach Distribution eine etwas ältere Version von Node.js oder lädt die aktuellste Version direkt von der Download-Seite. Die ganz Harten kompilieren selbst, wie das auf einem Raspberry aussieht erkläre ich in meinem Blog: http://www.devils-heaven.com/raspberry-pi-installing-node-js/

Nach der Installation hüpft man am Besten gleich in die Kommandozeile/Shell/Whatever und prüft mit zwei einfachen Befehlen ob die Installation geklappt hat:

npm -v
node -v

npm ist der Node.js package manager, mit ihm werden zusätzliche Module installiert, aber auch praktische Tools wie Bower oder Gulp. Mittlerweile setzt sich der package manager auch immer mehr im Frontend-Bereich durch und ersetzt damit Bower. Isomorphic JavaScript ist das Zauberwort, die Grenzen zwischen Backend und Frontend verschwimmen. Codeteile können einmal programmiert und danach auf Server und Client gleichzeitig verwendet werden. Voraussetzung dafür ist die CommonJS Spezifikation, welche mittlerweile über Tools wie Browserify oder Webpack auch am Client Einzug gehalten hat:

var clientServerModule = require('client-server-module');

PHP vs Node.js

Eigentlich ist das nicht ganz richtig, man müsste „Node.js vs PHP + Apache“ vergleichen. PHP braucht unbedingt einen Server als Unterbau, bei Node.js ist der eigene Code direkt der Server. Das lässt sich mit einem einfachen Beispiel erklären, wenn der Apache einmal läuft dann ist ein „Hello World“ in PHP schnell erstellt (ich verzichte der Einfachheit halber auf den gesamten HTML Code):

<?php
echo 'Hello World';

In Node.js sieht das Beispiel schon etwas komplexer aus:

var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337, '127.0.0.1');

Das ist jetzt nicht unbedingt ein werbendes Beispiel für Node.js, allerdings muss man bedenken dass kein eigener Webserver dafür notwendig ist, dieser wird per Node.js erstellt („createServer“) und „horcht“ danach laufend auf Requests, und zwar auf Port 1337. „Mehr Kontrolle“ ist also ein entscheidendes Merkmal von Node.js.

Bei einem PHP Server wird die Datei angelegt und ist danach sofort im Web abrufbar. Bei Node.js muss allerdings das Script gestartet werden und im Hintergrund laufen. Das passiert ganz einfach in der Kommandozeile/Shell:

node server.js

Die Dateiendung „.js“ ist nicht zwingend notwendig, Node.js weiss vollautomatisch dass der Parameter eine JavaScript Datei ist. So weit sind wir schon mit der Technik, Wahnsinn!

Nach dem Aufruf läuft der kleine Node.js Server so lange bis man ihn per STRG-C beendet. Bei einer Änderung im Code muss demnach der Server neu gestartet werden, ebenso bei einem Absturz der Applikation. Klingt mühsam, ist es auch. Allerdings gibt es Abhilfe in Form eines praktischen Tools welches gleich mehrere Probleme löst:

PM2

PM2 ist ein praktischer „Prozessmanager“ für Node.js, welcher bei Absturz der Applikation diese automatisch neu starten kann. Aber das ist nur ein kleiner (und natürlich durchaus wichtiger) Vorteil von PM2:

Cluster Mode

Node.js nutzt prinzipiell nur einen CPU-Core, kann jedoch mit dem (nicht ganz produktionsreifen) cluster-Modul auch mit mehreren Cores umgehen. PM2 nutzt dieses Modul automatisch im Cluster Mode, somit muss der Programmcode nicht umgeschrieben werden und load balancing ist mit Minimalaufwand möglich. Hier ein einfaches Beispiel für den Cluster Mode, mit 2 CPU-Cores:

Zu beachten ist natürlich der Datenaustausch zwischen den einzelnen clustern, hier ist der Einsatz von Redis eine interessante Lösung.

Der Cluster Mode hat durch die Aufteilung auf mehrere Cores aber auch andere Vorteile:

Clustering zeigt seine wahre Stärke natürlich in erster Linie bei CPU-intensiven Tätigkeiten. Monitoring ist in der Hinsicht natürlich sehr wichtig und ebenfalls in PM2 integriert:

Detaillierte Infos zu PM2 und eine Auflistung aller Befehle finden sich in der npm registry: https://www.npmjs.com/package/pm2

Asynchrone Dateizugriffe

Gerade bei Applikationen mit vielen Dateizugriffen spielt Node.js seine Stärken aus. Dateioperationen (vor allem in Bezug auf HDDs) dauern lange – Node.js erlaubt sogenanntes „non-blocking I/O„, d.h. es wird eine Dateioperation angestossen, aber nicht darauf darauf gewartet. Nachfolgender Code wird sofort ausgeführt und das Ergebnis des Dateizugriffs wir per asynchroner Callback-Funktion übermittelt:

var fs = require('fs');

fs.writeFile('helloworld.txt', 'Hello World!', function (err) {
    if (err) throw err;
    fs.readFile('helloworld.txt', function (err, data) {
        if (err) throw err;
        console.log('third comment to shop up');
    });
    console.log('second comment to show up');
});
console.log('first comment to show up');

Der Code schreibt „Hello World!“ in die Datei „helloworld.txt“ und liest danach den Inhalt wieder aus. Hier sieht man auch wunderbar die Verschachtelung mehrerer Callback-Funktionen, die zur berühmten „Callback Hell“ oder „Pyramid of Doom“ führen. Ich empfehle eine tiefgreifende Auseinandersetzung mit den Themen „asynchrones JavaScript“ und „Callback Hell“, damit sollte jeder JavaScript-Programmierer fit sein.

Node.js braucht natürlich zur Ausführung der asynchronen Funktionen genauso Prozessorzeit und kann zum Beispiel nicht zwei Callback-Funktionen gleichzeitig abhandeln, folgende Aussage ist hierfür sehr treffend:

everything runs in parallel except your code

Express Application Framework

Für umfangreichere Applikationen existieren eine Reihe von Frameworks/Modulen für Node.js, das wohl bekannteste ist Express. Die simple Aufgabe statische Ressourcen (.html, .js, .css, Bilder, …) vom Server an den Client zu senden wäre in purem Node.js relativ umständlich. Per Express ist das ein Kinderspiel:

var express = require('express'),
    app = express(),
    server;

app.use(express.static('public'));

var server = app.listen(3000, function () {
    var host = server.address().address;
    var port = server.address().port;
    console.log('Example app listening at http://%s:%s', host, port);
});

Zeile 5 ist hier wesentlich, damit bekommt der Client schon Zugriff auf alle statischen Ressourcen im Ordner „public“. Eine simple Web App welche nur mit HTML, CSS und JavaScript und ohne Server-Programmierung auskommt kann damit bereits betrieben werden.

Express kann aber noch viel mehr, hier ein Routing-Beispiel mit GET und POST Anfragen:

var express = require('express'),
    app = express(),
    server;
	
app.use(express.static('public'));

app.route('/login')
	.all(function(req, res, next) {
		//this runs before get or post
		next();
	})
	.get(function(req, res, next) {
		//send login form
		res.send('Hello World!');
	})
	.post(function(req, res, next) {
		//check user login and redirect
		res.redirect('/');
	});
	
var server = app.listen(3000, function () {
    var host = server.address().address;
    var port = server.address().port;
    console.log('Example app listening at http://%s:%s', host, port);
});

Die Beispiele kratzen natürlich nur an der Oberfläche, Express spielt auch mit verschiedenen Template-Engines gut zusammen. Jade wird in dem Zusammenhang immer wieder erwähnt, ich bevorzuge eher die HTML-ähnlichen Templates von Handlebars.

Web Sockets per Socket.io

Die sehr einfache Handhabung von Web Sockets ist ebenfalls ein entscheidender Vorteil von Node.js. Das Framework Socket.IO bietet sogar automatisch Fallbacks, wenn der Client-Browser keine Web Sockets unterstützt. Die Fallback-Möglichkeiten werden in Mixu’s Node Book wunderbar erklärt. Der Code auf Server und Client ist sehr simpel:

server.js

var express = require('express'),
    app = express(),
    http = require('http').Server(app),
    io = require('socket.io')(http);

app.get('/', function(req, res) {
    res.sendFile(__dirname + '/index.html');
});

io.on('connection', function(socket) {
    console.log('a user connected');
    socket.on('disconnect', function() {
        console.log('user disconnected');
    });
    socket.on('chatMessage', function(msg){
        console.log('message: ' + msg);
        io.emit('chatMessage', msg);
    });
});

http.listen(3000, function() {
    console.log('listening...');
});

index.html

<!doctype html>
<html>
<head>
    <title>Socket.IO Chat</title>
    <style>
        * {box-sizing: border-box;}
        body {font: 13px Helvetica, Arial;}
        form {background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%;}
        form input {border: 0; padding: 10px; width: 90%; margin-right: .5%;}
        form button {width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; cursor: pointer;}
        #messages {list-style-type: none; margin: 0; padding: 0;}
        #messages li {padding: 5px 10px;}
        #messages li:nth-child(odd) {background: #eee;}
    </style>
</head>
<body>
    <ul id="messages"></ul>
    <form action="" onsubmit="return formSubmit();">
        <input id="msg" autocomplete="off" /><button type="submit">Send</button>
    </form>
    
    <script src="/socket.io/socket.io.js"></script>
    <script>
        var socket = io(),
            inputMessage = document.getElementById('msg')
            messages = document.getElementById('messages');
        
        socket.on('chatMessage', function(msg){
            var newMessage = document.createElement('li');
            newMessage.innerHTML = msg;
            messages.appendChild(newMessage);
        });
        
        function formSubmit() {
            socket.emit('chatMessage', inputMessage.value);
            inputMessage.value = '';
            return false;
        }
    </script>
</body>
</html>

Der Code sollte grossteils selbsterklärend sein, zu beachten sind am Server vor allem die console.log-Einträge. Schliesst ein Benutzer sein Browserfenster, so wird automatisch ein „disconnect“ Event am Server ausgelöst. Am Client ist zu beachten dass ein Socket Server automatisch das Script socket.io.js bereitstellt („/socket.io/socket.io.js„). Alternativ kann die Client-Bibliothek aber auch von einem CDN bezogen werden, wie hier erklärt: http://socket.io/download/

Natürlich lässt sich in diesem Beispiel noch vieles optimieren, zum Beispiel könnte man die letzten Messages am Server speichern damit neue User diese ebenfalls bekommen. Man könnte Räume erstellen (Socket.IO bietet hierfür bereits ein Feature), Privat-Chats zwischen zwei Personen, Registierungsmöglichkeiten usw.


Man muss definitiv umdenken wenn man vorher mit PHP gearbeitet hat, aber der Einstieg ist sehr einfach. Node.js wird jedenfalls PHP noch lange nicht verdrängen und vorhandene PHP-Projekte per Node.js neu umzusetzen macht wenig Sinn. Vor allem für neue Applikationen und spezielle Anwendungsfälle sollte man aber überlegen auf Node.js zu setzen. Auch eine Kombination von PHP und Node.js kann unter Umständen zielführend sein.
Wie immer gilt: use the right tool for the job“.

Hier noch ein Auszug der Vor- und Nachteile von Node.js:

Vorteile

Nachteile

Die mobile Version verlassen