Kategorien
JavaScript & jQuery Programmierung

Aus der Praxis: Wir bauen eine Chatroom Web-App mit Node.js (Teil 3)

In dieser Node.js Tutorial-Serie zeigen wir, wie man eine Node.js-getriebene Echtzeit-Chatroom Web App erstellt, die vollständig cloud-fähig ist. Es wird darum gehen, Node.js auf einem Windows-Rechner aufzusetzen, ein Web-Frontend mit Express zu entwickeln, eine Node-Express-App auf Azure zu bringen, Socket.IO zu nutzen, um einen Echtzeit-Layer hinzuzufügen und zu zeigen, wie man das Ganze zum Schluss einsetzt.

Aus der Praxis: Wir bauen eine Chatroom Web-App mit Node.js (Teil 3)

Schwierigkeitsstufe: Einsteiger bis mittlere Kenntnisse, vorausgesetzt werden HTML5 und JavaScript.

Teil 3 – Ein Chatroom Backend mit Node.js, Socket.IO und Mongo aufsetzen

Willkommen zurück zu Teil 3 der Praxis-Tutorialserie zu Node.js: Wir programmieren eine Node.js-getriebene Chatroom-Web-App. In diesem Teil erkläre ich, wie man die bestehende Express-basierte Node.js-App nutzt, um ein Chatroom-Backend mit WebSocket-Unterstützung zu erstellen.

Was sind WebSockets? Was ist Socket.IO?

WebSocket ist ein Netzwerkprotokoll, mit dem Web-Anwendungen eine bidirektionale Verbindung zwischen Webbrowser und Webserver über TCP aufbauen können. Es ist vollständig kompatibel mit HTTP und nutzt den TCP-Port 80. WebSocket ermöglicht Web-Anwendungen in Echtzeit und fortgeschrittene Interaktionen zwischen Client und Server. Das Protokoll wird von zahlreichen Servern unterstützt, darunter Edge und Internet Explorer, Google Chrome, Firefox, Safari und Opera.

Socket.IO ist eine JavaScript-Library und ein Node.js-Modul. Damit gelingt es sehr einfach und schnell, bidirektionale, event-basierte Kommunikations-Apps zu erstellen. Die Nutzung von WebSockets wird dadurch stark vereinfacht. Wir werden für unsere Chatroom-App Socket.IO v1.0 benutzen.

Socket.IO zu package.json hinzufügen

Package.json ist eine Datei, die verschiedene Metadaten enthält, die für das Projekt relevant sind, einschließlich ihrer Abhängigkeiten. NPM nutzt diese Datei, um Module zu laden, die für das Projekt benötigt werden. Hier lohnt es sich, einen Blick auf diese interaktive Erklärung von package.json und was alles drin steckt, zu werfen.
Nun fügen wir Socket.IO dem Projekt als Abhängigkeit hinzu. Das kann man auf zweierlei Weise tun.

1. Wer schon die anderen Teile der Tutorial-Serie verfolgt und bereits ein Projekt im Visual Studio Setup erstellt hat, braucht nur einen Rechtsklick auf den NPM-Teil des Projekts auszuführen, um dann “Install New npm Packages…” auszuwählen.

clip_image002

Nachdem sich das Fenster geöffnet hat, suchen wir nach “socket.io”, wählen das oberste Ergebnis aus und machen ein Häkchen bei “Add to package.json”. Dann klicken wir den Button “Install Package”. Nun wird Socket.IO in unserem Projekt installiert und zur package.json-Datei hinzugefügt.

clip_image004

package.json

{
  "name": "NodeChatroom",
  "version": "0.0.0",
  "description": "NodeChatroom",
  "main": "app.js",
  "author": {
    "name": "Rami Sayar",
    "email": ""
  },
  "dependencies": {
    "express": "3.4.4",
    "jade": "*",
    "socket.io": "^1.0.6",
    "stylus": "*"
  }
}

2. Wer mit OS X oder Linux arbeitet, muss den folgenden Befehl im Root-Verzeichnis des Projektordners ausführen, um das gleiche Ergebnis zu erhalten.

npm install --save socket.io

Socket.IO der app.js hinzufügen

Im nächsten Schritt fügen wir Socket.IO der app.js hinzu. Dazu suchen wir den folgenden Code:

http.createServer(app).listen(app.get('port'), function(){
  console.log('Express server listening on port ' + app.get('port'));
});

Und ersetzen ihn durch:

var serve = http.createServer(app);
var io = require('socket.io')(serve);

serve.listen(app.get('port'), function () {
    console.log('Express server listening on port ' + app.get('port'));
});

Auf diese Weise wird der HTTP-Server in einer Variable namens serve erfasst und übermittelt, so dass das Socket.IO Modul sich diesem zuordnet. Der letzte Block des Codes nimmt die Variable serve und führt die Funktion listen aus, welche den HTTP-Server startet.

Protokollieren, wenn User kommen und gehen

Gut wäre es natürlich, protokollieren zu können, wenn ein User den Chatroom betritt. Das gelingt mit dem folgenden Code. Er löst bei jedem connection Event eine Callback-Funktion via WebSocket an unseren HTTP-Server aus. In der Callback-Funktion rufen wir console.log auf, um zu protokollieren, dass ein User anwesend ist. Wir können diesen Code hinzufügen, nachdem wir serve.listen aufgerufen haben.

io.on('connection', function (socket) {
    console.log('a user connected');
});

Um auch festzuhalten, wann ein User den Chatroom verlässt, müssen wir das disconnect Event für jedes Socket verbinden. Wir fügen dazu den folgenden Code nach dem console.log des vorhergehenden Code-Blocks hinzu.

    socket.on('disconnect', function () {
        console.log('user disconnected');
    });

Und so sieht der Code dann aus:

io.on('connection', function (socket) {
    console.log('a user connected');
    socket.on('disconnect', function () {
        console.log('user disconnected');
    });
});

Senden einer Mitteilung aus dem Chat-Channel

Socket.IO besitzt eine Funktion namens emit, um Events zu übertragen.
Nach dem Empfang einer Mitteilung im Chat-Channel, wird sie durch den Aufruf von emit mit dem Sende-Marker in der Callback-Funktion an alle anderen Verbindungen auf diesem Socket gesendet.

socket.on('chat', function (msg) {
    socket.broadcast.emit('chat', msg);
});

So sieht der Code dann aus:

io.on('connection', function (socket) {
    console.log('a user connected');
    socket.on('disconnect', function () {
        console.log('user disconnected');
    });
 
    socket.on('chat', function (msg) {
        socket.broadcast.emit('chat', msg);
    });
});

Mitteilungen in einer NoSQL-Datenbank sichern

Der Chatroom sollte Chat-Mitteilungen in einem einfachen Datenspeicher sichern. Für gewöhnlich gibt es zwei Wege, um Datenbestände in Node zu speichern. Entweder man nutzt einen Datenbank-spezifischen Driver oder eine ORM. In diesem Tutorial werde ich MongoDB nutzen, um die Mitteilungen zu sichern. Natürlich gibt es viele weitere Möglichkeiten, einschließlich SQL-Datenbanken wie PostgreSQL oder MySQL.

Als erstes stellen wir sicher, dass wir eine MongoDB haben, mit der wir uns verbinden können. Diese kann man auch von Drittanbietern, wie MongoHQ oder MongoLab, hosten lassen. In diesem Tutorial wird sehr gut gezeigt, wie man eine MongoDB mit Hilfe des MongoLab-Add-On in Azure erstellt. Für unsere Zwecke reicht es hier, bis zu “Create the App” zu lesen. Hauptsache, wir haben die MONGOLAB_URI irgendwo gespeichert und können auf diese später leicht zugreifen.

Nachdem wir die MongoDB erstellt haben und MONGOLAB_URI für die Datenbank haben (zu finden unter Connection-Information in der Zwischenablage), werden wir dafür sorgen, dass der URI für die Anwendung verfügbar ist. Es ist nicht zu empfehlen, vertrauliche Informationen wie diesen URI in den Code oder in eine Konfigurationsdatei unseres Quellcode-Management-Tools einzubauen.

Wir können den Wert der Connection-Strings-Liste im Konfigurationsmenü unserer Azure-Web-Anwendung hinzufügen (wie im vorhergehenden Tutorial beschrieben) oder wir fügen ihn der App-Setting-Liste hinzu (mit dem Namen CUSTOMCONNSTR_MONGOLAB_URI). Auf unserem lokalen Rechner können wir den Wert den Umgebungsvariablen hinzufügen; mit dem Namen CUSTOMCONNSTR_MONGOLAB_URI und dem Wert des URI.

Im nächsten Schritt sorgen wir dafür, dass unser Projekt MongoDB unterstützt. Das gelingt, indem wir die folgende Zeile dem Abhängigkeiten-Objekt in package.json hinzufügen. Vergesst dabei nicht das Sichern der Änderungen in der Datei.

"mongodb": "^1.4.10",

Es folgt ein Rechtsklick im NPM-Teil der Projekts im Solution-Explorer, um das Kontextmenü anzeigen zu lassen. Jetzt “Install missing npm packages” klicken, um das MongoDB Paket zu installieren und es damit als Modul nutzbar zu machen.

clip_image005

Nun importieren wir dieses Modul, um das MongoDB-Client-Object in app.js nutzen zu können. Dafür fügen wir die folgenden Code-Zeilen nach den ersten require(‘’) Funktionsaufrufen ein, wie in Zeile 11.

var mongo = require('mongodb').MongoClient;

Nun wollen wir uns mit der Datenbank mit Hilfe des URI verbinden, das uns in der CUSTOMCONNSTR_MONGOLAB_URI-Umgebungsvariable vorliegt. Nachdem wir verbunden sind, fügen wir die Chat-Mitteilung ein, die über die Socket-Verbindungen empfangen wurde.

mongo.connect(process.env.CUSTOMCONNSTR_MONGOLAB_URI, function (err, db) {
    var collection = db.collection('chat messages');
    collection.insert({ content: msg }, function (err, o) {
        if (err) { console.warn(err.message); }
        else { console.log("chat message inserted into db: " + msg); }
    });
});

Deutlich im obigen Code zu sehen: Wir nutzen das process.env-Objekt, um den Wert der Umgebungsvariable zu erhalten. Nun begeben wir uns in eine Collection in der Datenbank und rufen die Insert-Funktion mit dem Inhalt in einem Objekt auf.

Nun wird jede Mitteilung in der MongoDB-Datenbank gespeichert.

Aussenden der zehn aktuellsten Mitteilungen

Natürlich sollen sich die User nicht total verloren fühlen, nachdem sie den Chatroom betreten haben. Daher ist es sicher sinnvoll, die letzten zehn erhaltenen Mitteilungen vom Server zu senden, um den Usern etwas Kontext zu geben. Dafür müssen wir Mongo verbinden. In diesem Fall nehme ich davon Abstand, den gesamten Socket-Code mit nur einer Verbindung in die Datenbank zu schicken. So ist sicher gestellt, dass der Server weiter arbeiten kann, auch wenn die Verbindung zur Datenbank mal verloren geht.

Um die Liste zu sortieren, und die Anfrage tatsächlich auf die zehn aktuellsten Mitteilungen zu beschränken, nutzen wir die von MongoDB generierte _id, da sie einen Zeitmarker enthält (in komplexeren Anwendungen ist es stets angebracht, einen Zeitmarker für jede Mitteilung zu haben). Jetzt rufen wir die limit-Funktion auf, um unsere Liste auf zehn Mitteilungen zu beschränken.

Wir streamen die Ergebnisse von MongoDB, damit sie an den Chatroom ausgesendet werden können, sobald sie eingetroffen sind.

mongo.connect(process.env.CUSTOMCONNSTR_MONGOLAB_URI, function (err, db) {
    var collection = db.collection('chat messages')
    var stream = collection.find().sort({ _id : -1 }).limit(10).stream();
    stream.on('data', function (chat) { socket.emit('chat', chat.content); });
});

Der obige Code erledigt den Job genau so, wie in den vorhergehenden Absätzen beschrieben.

Auf Azure anwenden

Um das Ganze wieder auf Azure laufen zu lassen, kann man einfach den Schritten folgen, die in den vergangenen Teilen des Tutorials beschrieben wurden (zum Beispiel in Teil 2).

Fazit

Nun haben wir ein Chat-System erstellt, das über WebSockets empfangene Mitteilungen an alle verbundenen Clients aussendet. Außerdem sichert es die Nachrichten in einer Datenbank und stellt die zehn aktuellsten Mitteilungen bereit, um allen Usern, die den Chatroom betreten, ausreichend Kontext zu geben.

Und jetzt heißt es: dran bleiben. Denn Teil 4 — Eine Chatroom-UI mit Bootstrap erstellen — wird schon bald hier bei Dr. Web veröffentlicht. Wer mehr Neuigkeiten dazu und zu anderen Artikeln erfahren will, kann mir einfach auf Twitter folgen @ramisayar.

Weitere Teile der Serie bei MSDN (in englischer Sprache)

Part 1 –  Introduction to Node.js
Part 2  – Welcome to Express with Node.js and Azure
Part 3 – Building a Backend with Node, Mongo and Socket.IO
Part 4 – Building a Chatroom UI with Bootstrap
Part 5 – Connecting the Chatroom with WebSockets
Part 6 – The Finale and Debugging Remote Node Apps

Noch mehr Lernen über Node auf Azure

Ausführlichere Lehrinhalte zu Node gibt es in meinem Kurs an der Microsoft Virtual Academy.

Sehr hilfreich sind auch diese Kurzvideos zu ähnlichen Node-Themen:

Dieser Artikel ist Teil der Web-Dev Tech-Series von Microsoft. Wir freuen uns Microsoft Edge (früher Project Spartan genannt) und seine neue Rendering Engine mit euch zu teilen. Kostenlose Virtual Machines oder Remote Testings für Mac, iOS, Android oder Windows gibt es hier: @ dev.modern.IE.

Zur besseren Übersicht und als Extra-Service folgt hier der gesamte Quellcode der app.js

//
/**
 * Module dependencies.
 */

var express = require('express');
var routes = require('./routes');
var user = require('./routes/user');
var http = require('http');
var path = require('path');

var mongo = require('mongodb').MongoClient;

var app = express();

// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.json());
app.use(express.urlencoded());
app.use(express.methodOverride());
app.use(app.router);
app.use(require('stylus').middleware(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
  app.use(express.errorHandler());
}

app.get('/', routes.index);
app.get('/users', user.list);

var serve = http.createServer(app);
var io = require('socket.io')(serve);

serve.listen(app.get('port'), function () {
    console.log('Express server listening on port ' + app.get('port'));
});

io.on('connection', function (socket) {
    console.log('a user connected');

    mongo.connect(process.env.CUSTOMCONNSTR_MONGOLAB_URI, function (err, db) {
        var collection = db.collection('chat messages')
        var stream = collection.find().sort({ _id : -1 }).limit(10).stream();
        stream.on('data', function (chat) { socket.emit('chat', chat); });
    });

    socket.on('disconnect', function () {
        console.log('user disconnected');
    });

    socket.on('chat', function (msg) {
        mongo.connect(process.env.CUSTOMCONNSTR_MONGOLAB_URI, function (err, db) {
            var collection = db.collection('chat messages');
            collection.insert({ content: msg }, function (err, o) {
                if (err) { console.warn(err.message); }
                else { console.log("chat message inserted into db: " + msg); }
            });
        });

        socket.broadcast.emit('chat', msg);
    });
});

(dpe)

Von Rami Sayar

Rami Sayar ist technischer Evangelist bei Microsoft Kanada und dort für "Web Development" (JavaScript, AngularJS, Node.js, HTML5, CSS3, D3.js, Backbone.js, Babylon.js), Open Data und Open Source Technologien (Python, PHP, Java, Android, Linux, etc.) zuständig. Er betreibt einen Blog und ist als @ramisayar auf Twitter aktiv.

2 Antworten auf „Aus der Praxis: Wir bauen eine Chatroom Web-App mit Node.js (Teil 3)“

Schreibe einen Kommentar zu Karl Marx Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.