Echtzeitfähig: Socket.io und AngularJS verbinden

WebSockets sind eine wunderbare Basis für Single-Page-Anwendungen. Daher liegt es nahe, Socket.io und AngularJS zu integrieren. Allerdings gilt es dabei, den internen Lebenszyklus von AngularJS zu beachten. Ein kleiner Dienst zeigt, wie das funktioniert.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 4 Min.
Von
  • Golo Roden
Inhaltsverzeichnis

Wie früher ausführlich beschrieben, ruft AngularJS nach jeder Zustandsänderung die Funktion $apply auf, um die Werte der Steuerelemente mit jenen der Controller abzugleichen. Das funktioniert auf automatischem Wege jedoch nur für die Bereiche einer Anwendung, die AngularJS überwachen kann.

Die von Node.js-Entwicklern regelmäßig verwendete Bibliothek Socket.io zur Integration von WebSockets zählt jedoch nicht dazu, weshalb AngularJS den Zustand der Anwendung nach dem Eintreffen von WebSocket-Nachrichten nicht automatisch aktualisiert. Als Ausweg bietet sich der Aufruf der $apply-Funktion von Hand an, was allerdings gerne vergessen wird.

Eine mögliche Alternative besteht darin, Socket.io nicht direkt in den Controller einzubinden, sondern hierfür einen Dienst zu verwenden. Wenn dieser Dienst darüber hinaus auch den Aufruf der $apply-Funktion übernimmt, wäre er auf die gleiche Art verwendbar wie der von AngularJS standardmäßig angebotenen $http-Dienst.

Glücklicherweise ist ein derartiger Dienst zügig implementiert. Zunächst registriert man mit der factory-Funktion von AngularJS einen neuen Dienst und übergibt diesem die Variable $rootScope als Parameter. Da AngularJS einen Dienst nicht an einen konkreten Controller koppelt, muss man den Weg über diesen Scope wählen. Der erste Schritt innerhalb dieses Dienstes besteht in der Initialisierung von Socket.io.

app.factory('ws', [ '$rootScope', function ($rootScope) {
'use strict';
var socket = io.connect();
}]);

Anschließend muss man für diesen Dienst die für Socket.io erforderlichen Funktionen wie emit und on implementieren. Deren Callback ist danach in Verbindung mit einem Aufruf der $apply-Funktion des $rootScope-Objekts auszuführen.

app.factory('ws', [ '$rootScope', function ($rootScope) {
'use strict';
var socket = io.connect();

return {
on: function (event, callback) {
socket.on(event, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(null, args);
});
});
}
};
}]);

Wie üblich gilt auch in diesem Fall, dass man gut beraten ist, eine an ein Ereignis angehängte Funktion wieder zu entfernen, sobald das Ereignis nicht mehr von Interesse ist. Hierfür eignet sich eine entsprechende off-Funktion, die intern die Funktion removeListener des verwendeten Sockets aufruft.

app.factory('ws', [ '$rootScope', function ($rootScope) {
'use strict';
var socket = io.connect();

return {
on: function (event, callback) { /* ... */ },

off: function (event, callback) {
socket.removeListener(event, callback);
}

};
}]);

Außer den Funktionen on und off benötigt man auch eine Funktion emit, um ein Ereignis mit Socket.io auszulösen und an den Webserver zu übertragen. Da auch diese Funktion einen Callback entgegennimmt, muss man diesen wiederum in einem entsprechenden Aufruf der $apply-Funktion kapseln.

app.factory('ws', [ '$rootScope', function ($rootScope) {
'use strict';
var socket = io.connect();

return {
emit: function (event, data, callback) {
socket.emit(event, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(null, args);
}
));
});
},


on: function (event, callback) { /* ... */ },

off: function (event, callback) { /* ... */ }
};
}]);

Innerhalb eines Controllers kann man den ws-Service auf einfache Art verwenden, indem man ihn als Abhängigkeit angibt. Wichtig ist, darauf zu achten, zunächst auf das erfolgreiche Verbinden zu warten und erst dann Ereignisse auszulösen.

app.controller('dahsboardController', [
'ws', '$scope', function (ws, $scope) {
'use strict';
ws.on('connect', function () {
ws.emit('getState', { initial: true });

ws.on('gotState', function (state) {
$scope.state = state;
});
});
}]);

Wenn ein Controller nicht länger benötigt wird, beispielsweise weil der Anwender auf eine andere Webseite wechselt, löst AngularJS intern das $destroy-Ereignis aus. Dieses Ereignis stellt daher den geeigneten Zeitpunkt dar, um an Ereignisse angehängte Funktionen wieder zu entfernen.

app.controller('dahsboardController', [
'ws', '$scope', function (ws, $scope) {
'use strict';
ws.on('connect', function () {
ws.emit('getState', { initial: true });

ws.on('gotState', updateState);

$scope.$on('$destroy', function () {
ws.off('gotState', updateState);
});
});
}]);

tl;dr: Um Socket.io mit AngularJS zu integrieren, genügt ein kleiner Dienst, der die Funktionen von Socket.io kapselt und diese in einem geeigneten Scope ausführt. Wichtig ist, darauf zu achten, an Ereignisse angehängte Funktionen auch wieder zu entfernen, wenn man diese nicht mehr benötigt. ()