Backbone.Js und Socket.IO

Das Ziel von Backbone.js ist es, Struktur in eine Javascript Anwendung zu bringen. Zu diesem Zweck stellt es verschiedene Komponenten zur Verfügung, mit deren Hilfe sich das Model-View-Controller Pattern relativ schnell und einfach in einer Javascript Anwendung umsetzen lässt. Um Daten zu persistieren oder vorhandene Daten in die Applikation zu laden, wird in den meisten Fällen eine Serverkomponente benötigt. Backbone.js sieht eine RESTful API zum Server vor, um diese Kommunikation durchzuführen. Die Entwickler von Backbone.js haben sich dazu entschlossen möglichst Komponenten aus existierenden Bibiotheken zu verwenden. So wird zum Zweck der Kommunikation mit einem Server beispielsweise die ajax-Komponente von jQuery verwendet.

Der Nachteil von ajax ist, dass die Kommunikation vom Client initiiert werden muss. Will man Änderungen vom Server an den Client schicken, ist dies nicht ohne Weiteres möglich. Ein Ansatzpunkt um diese Problemstellung zu bewältigen ist der Einsatz von Long-Polling. Hierbei öffnet der Client eine Verbindung zum Server und hält diese für eine bestimmte Zeitspanne offen. Über diesen offenen Kanal kann dann der Server den Client über Änderungen benachrichtigen. Auf diese Weise kann man Echtzeit-Webanwendungen simulieren. Eine wirklich elegante Lösung stellt dieser Ansatz allerdings nicht dar.

Mit der Einführung von Websockets gibt es für das Problem der bidirektionalen Kommunikation zwischen Server und Client einen sauberen Lösungsansatz, der genau auf diese Klasse von Problemen zugeschnitten ist.

Eine Schwierigkeit, die in diesem Zusammenhang existiert, liegt darin, dass Websockets nur von modernen Browsern unterstützt werden. Häufig kann man sich als Anbieter einer Webapplikation nicht erlauben zu viele Clients auszuschließen. Eine Lösung hierfür bietet die Bibliothek Socket.io. Sie stellt Schnittstellen zur Verwendung von Websockets zur Verfügung und hält einen Fallback-Mechanismus bereit, falls Websockets vom verwendeten Browser nicht unterstützt werden. Es werden folgende Technologien unterstützt: websocket, flashsocket, htmlfile (eine Lösung über ein IFrame), xhr-polling und jsonp-polling.

Die Verwendung von Socket.IO gestaltet sich sehr einfach. Über den Befehl “connect” wird eine Verbindung hergestellt. Mittels Parameter wird angegeben zu welchem Server sich der Client verbinden soll. Die “connect”-Methode gibt einen Socket zurück, mit dem im Weiteren gearbeitet wird. Die wichtigsten Methoden im Zusammenhang mit Socket.IO sind “on” und “emit”.

var socket = connect('example.org:1337');

“on” erwartet als ersten Parameter das Event, auf das subscribed werden soll und als zweiten Parameter einen Callback, der ausgeführt wird, wenn das Event eintritt.

socket.on('myEvent', function (data) { ... });

“emit” wird dazu verwendet, Events zu triggern und so die damit verbundenen Callbacks auszulösen. Emit erwartet neben dem Namen des Events, das ausgelöst werden soll, eine Datenstruktur, die an die verbundenen Callbacks als Eingabeparamter weitergereicht wird.

socket.emit('myEvent', {data: 'Hello World'});

Auch die Serverseite kann mit Socket.IO abgedeckt werden. Da Socket.IO komplett in Javascript implementiert ist, wird eine entsprechende Javascript-Engine benötigt. Zur Umsetzung eines Websocket-Servers eignet sich beispielsweise node.js, die V8 Engine von Google für Server. Um Socket.IO unter node.js zu installieren, verwendet man am besten npm, den Paketmanager von node.js. Durch einen einfachen Befehl lassen sich damit zusätzliche Pakete installieren.

npm install socket.io

Ist Socket.IO nun auf dem Server installiert, lässt sich mit wenigen Zeilen die Serverkomponente umsetzen.

var io = require('socket.io').listen(1337);

io.sockets.on('connection', function (socket) {
    socket.on('test', function (data) {
        socket.emit('testReply', {data: 'Hello World'});
    });
});

Der Befehl “listen” lässt Socket.IO auf dem angegebenen Port lauschen. Über die Bindung des “connection” Events wird der Server mit Leben befüllt. Hier werden die einzelnen Callbacks für bestimmte Events einer Verbindung definiert. Auf Serverseite werden die Methoden “on” und “emit” auf die gleiche Weise verwendet wie beim Client.

Es stellt sich nun die Frage: Wie kann man Backbone.js und Socket.IO so verbinden, dass eine Realtime Anwendung umgesetzt werden kann?

Als Ansatzpunkt empfiehlt sich hier die Komponente von Backbone.js, die für die Kommunikation mit dem Server verantwortlich ist: Backbone.sync. Tauscht man diese Methode durch eine eigene Implementierung aus, hat man die Möglichkeit, sämtliche Lese- und Schreiboperationen, die Backbone durchführt, abzufangen und sie über Websockets zu leiten.

Backbone.sync = function(method, model, options) {...}

Der Parameter “method” stellt die Aktion dar, die durchgeführt werden soll. Hier stehen die folgenden Stringwerte zur Verfügung: “create“, “read“, “update“ und “delete“. Backbone übersetzt diese Aktionen in ihre HTTP-Entsprechung, so wird beispielsweise aus “create“ ein “POST“ und aus “read“ ein “GET“. Der zweite Parameter stellt die Instanz der Modelklasse dar, auf die die Aktion ausgeführt werden soll. Der dritte Parameter, options, schließlich enthält unter anderem die Callbacks, die bei Erfolg oder im Fehlerfall ausgeführt werden sollen.

Im ersten Schritt benötigt man die “url” Property des Models, um entscheiden zu können, wohin die Anfrage geschickt werden soll. In der Implementierung für Websockets kann die URL als Eventbezeichnung verwendet werden. Das einzige Problem, das hier auftritt, ist, dass “url” sowohl eine Property als auch eine Methode sein kann. Um damit umzugehen, benötigt man eine kleine Fallunterscheidung.

var url = model.url;
if( _.isFunction(model.url)) {
    url = model.url();
}

Danach wäre man theoretisch schon in der Lage etwas zu verschicken. Zu diesem Zweck benötigt man lediglich eine Referenz auf den Socket. Für dieses Beispiel verwenden wir ein globales Socket-Objekt, über das die Kommunikation laufen kann.

Backbone.sync = function(method, model, options) {
    var url = model.url;
    if( _.isFunction(model.url)) {
        url = model.url();
    }
    socket.emit(url, payload);
}

Nun stellt sich noch die Frage, was an den Server gesendet werden muss. Als Erstes muss dem Server die Art der Aktion übermittelt werden, die durchgeführt werden soll, also der Wert des “method” Parameters von Backbone.sync. Außerdem muss das Model an den Server weitergegeben werden, damit gegebenenfalls veränderte Werte gespeichert werden können.

 var payload = {
    method: method,
    model: model
};

Das Kernkonzept von Javascript Architekturen ist der eventgetriebene Ansatz. Dieser macht es notwendig, dass auf das Auftreten bestimmter Ereignisse Callbacks gebunden werden können. Soll Backbone.sync komplett ersetzt werden, ist es notwendig, auch mit den Callbacks umzugehen, die einem “save” eines Models mitgegeben werden können.

Um dieses Problem lösen zu können, ist es notwendig, die Callbacks zu registrieren und bei einer Rückantwort des Servers auf diese zu referenzieren. Als Registry dient in diesem Fall eine globale Datenstruktur, die folgendermaßen aufgebaut ist:

registry {
    lastid: 0,
    callbacks: []
}

Innerhalb der neuen Implementierung von Backbone.sync wird zuerst geprüft, ob die Registry bereits gefüllt ist. Ist dies nicht der Fall, wird diese initialisiert. Jeder Aufruf von Backbone.sync führt dazu, dass die id inkrementiert wird und so eine eindeutige Zuordnung zu den Callbacks geschaffen wird. Die id wird in der Registry vorgehalten, um einfacher darauf zugreifen zu können.

if (null == register) {
    register = {
        lastid: null,
        callbacks: {}
    };
} else {
    register.lastid += 1;
}
register.callbacks[register.lastid] = {
    success: options.success,
    error: options.error
};

Um die korrekten Callbacks aufrufen zu können, ist es notwendig die id der Callbacks während der gesamten Kommunikation mitzuführen. Das bedeutet, dass die “payload” Variable um einen weiteren Schlüssel, die “id”, erweitert werden muss:

var payload = {
    ...
    id: register.lastid
};

Auf Serverseite sieht die Lösung folgendermaßen aus: Ist der Server fertig mit der Bearbeitung der Anfrage, triggert er über die “emit” Method von Socket.IO das Event “reply”. Dieses enthält neben den Daten, die der Client durch die Aktion erhält, eine Eigenschaft “id”, die als Referenz auf die Callbacks dient und zusätzlich die Information, welcher der Callbacks, also “error” oder “success”, ausgeführt werden soll.

socket.on('reply', function(data) {
    if (register.callbacks.hasOwnProperty(data.id)) {
        if (register.callbacks[data.id].hasOwnProperty(data.cb)) {
            var func = register.callbacks[data.id][data.cb];
            if (_.isFunction(func)) {
                func(data.payload);
            }
        }
        delete(register.callbacks[data.id]);
    }
});

Diese Funktion prüft, ob die angegebene id in der Registry vorhanden ist und ob der gewünschte Callbacktyp existiert. Danach wird verifiziert, ob es sich bei dem angefragten Element tatsächlich um eine ausführbare Funktion handelt. Ist dies alles sichergestellt, wird der Callback mit der “payload” Property der Antwort aufgerufen und im Anschluss sämtliche Callbacks, die unter der angegebenen id registriert sind, gelöscht.

Mit der beschriebenen Vorgehensweise ist es möglich, Backbone.js so umzustellen, dass statt der xhr-Requests Websockets verwendet werden können. Dieses Beispiel zeigt nur im Ansatz, wie die eigentliche Lösung aussehen sollte und ist nicht für den Produktivbetrieb geeignet. Für diesen Fall sollten keine globalen Variablen verwendet werden und auch die Ausführung und der Zugriff auf Callbacks muss eingeschränkt werden.

Neben den Umbauten an Backbone.sync steht nun durch Socket.IO auch die Möglichkeit zur Verfügung Models auf bestimmte Events lauschen zu lassen und vom Server Änderungen an Models durchzuführen, die durch das Eventsystem von Backbone über die View an den Benutzer weitergegeben werden und so Echtzeit Webanwendungen ermöglichen.

Für neue Blogupdates anmelden:


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.