AngularJS-Anwendung mit Socket.IO schreiben

Benjamin Ford
Brian Ford

Einleitung

AngularJS ist ein praktisches JavaScript-Framework, das eine bidirektionale Datenbindung bietet, die sowohl einfach als auch schnell ist, ein leistungsstarkes Anweisungssystem, mit dem Sie wiederverwendbare benutzerdefinierte Komponenten erstellen können, und vieles mehr. Socket.IO ist ein browserübergreifender Wrapper und Polyfill für WebSockets, der die Entwicklung von Echtzeitanwendungen zum Kinderspiel macht. Übrigens funktionieren die beiden ziemlich gut zusammen!

Ich habe bereits über das Schreiben einer AngularJS-Anwendung mit Express geschrieben. Dieses Mal geht es jedoch um die Integration von Socket.IO, um einer AngularJS-Anwendung Echtzeitfunktionen hinzuzufügen. In dieser Anleitung geht es um das Schreiben einer Instant Messaging-App. Sie baut auf meiner vorherigen Anleitung auf, bei der ein ähnlicher node.js-Stack auf dem Server verwendet wird. Wenn Sie also nicht mit Node.js oder Express vertraut sind, sollten Sie dies zuerst prüfen.

Demo öffnen

Du kannst das fertige Produkt wie immer auf GitHub herunterladen.

Voraussetzungen

Es gibt einige Textbausteine, um Socket.IO einzurichten und in Express einzubinden. Deshalb habe ich den Angular Socket.IO Seed erstellt.

Sie können entweder das Repository „Angular-node-seed“ aus GitHub klonen, um zu beginnen:

git clone git://github.com/btford/angular-socket-io-seed my-project

oder als ZIP-Datei herunterladen.

Sobald Sie den Seed haben, müssen Sie mit npm ein paar Abhängigkeiten abrufen. Öffnen Sie ein Terminal im Verzeichnis mit dem Startwert und führen Sie folgenden Befehl aus:

npm install

Wenn diese Abhängigkeiten installiert sind, können Sie die grundlegende Anwendung ausführen:

node app.js

und prüfen Sie sie in Ihrem Browser unter http://localhost:3000, um sicherzustellen, dass sie wie erwartet funktioniert.

Entscheidung für App-Funktionen

Es gibt mehr als nur unterschiedliche Möglichkeiten, eine Chat-Anwendung zu schreiben. Im Folgenden werden die Mindestfunktionen beschrieben, die unsere Chat-Anwendung haben wird. Es wird nur einen Chatroom geben, dem alle Nutzer angehören sollen. Nutzer können ihren Namen auswählen und ändern, aber die Namen müssen eindeutig sein. Der Server erzwingt diese Eindeutigkeit und benachrichtigt Nutzer, wenn sie ihre Namen ändern. Der Client sollte eine Liste mit Nachrichten und eine Liste der Nutzer anzeigen, die sich derzeit im Chat-Raum befinden.

Ein einfaches Frontend

Mit dieser Spezifikation können wir mit Jade ein einfaches Frontend erstellen, das die erforderlichen UI-Elemente bereitstellt. Öffnen Sie views/index.jade und fügen Sie Folgendes in block body ein:

div(ng-controller='AppCtrl')
.col
  h3 Messages
  .overflowable
    p(ng-repeat='message in messages') : 

.col
  h3 Users
  .overflowable
    p(ng-repeat='user in users') 

.clr
  form(ng-submit='sendMessage()')
    | Message: 
    input(size='60', ng-model='message')
    input(type='submit', value='Send')

.clr
  h3 Change your name
  p Your current user name is 
  form(ng-submit='changeName()')
    input(ng-model='newName')
    input(type='submit', value='Change Name')

Öffnen Sie public/css/app.css und fügen Sie den CSS-Code hinzu, um Spalten und Überläufe bereitzustellen:

/* app css stylesheet */

.overflowable {
  height: 240px;
  overflow-y: auto;
  border: 1px solid #000;
}

.overflowable p {
  margin: 0;
}

/* poor man's grid system */
.col {
  float: left;
  width: 350px;
}

.clr {
  clear: both;
}

Mit Socket.IO interagieren

Obwohl Socket.IO eine io-Variable im window zur Verfügung stellt, ist es besser, sie in das Dependency Injection-System von AngularJS zu kapseln. Wir beginnen mit dem Schreiben eines Dienstes, um das von Socket.IO zurückgegebene socket-Objekt zu verpacken. Das ist genial, denn so lässt sich der Controller später viel leichter testen. Öffnen Sie public/js/services.js und ersetzen Sie den Inhalt durch:

app.factory('socket', function ($rootScope) {
  var socket = io.connect();
  return {
    on: function (eventName, callback) {
      socket.on(eventName, function () {  
        var args = arguments;
        $rootScope.$apply(function () {
          callback.apply(socket, args);
        });
      });
    },
    emit: function (eventName, data, callback) {
      socket.emit(eventName, data, function () {
        var args = arguments;
        $rootScope.$apply(function () {
          if (callback) {
            callback.apply(socket, args);
          }
        });
      })
    }
  };
});

Beachten Sie, dass wir jeden Socket-Callback in $scope.$apply verpacken. Dadurch wird AngularJS angewiesen, den Status der Anwendung zu überprüfen und die Vorlagen zu aktualisieren, wenn nach dem Ausführen des an sie übergebenen Callbacks eine Änderung aufgetreten ist. Intern funktioniert $http auf die gleiche Weise. Nachdem einige XHR-Daten zurückgegeben wurden, wird $scope.$apply aufgerufen, sodass AngularJS seine Ansichten entsprechend aktualisieren kann.

Beachten Sie, dass dieser Dienst nicht die gesamte Socket.IO API umschließt (die links als Übung für den Leser ;P ). Allerdings behandelt er die in dieser Anleitung verwendeten Methoden und sollte Sie in die richtige Richtung weisen, wenn Sie sie erweitern möchten. Ich schreibe vielleicht noch einmal einen vollständigen Wrapper, aber das würde den Rahmen dieser Anleitung sprengen.

Jetzt können wir in unserem Controller nach dem socket-Objekt fragen, ähnlich wie bei $http:

function AppCtrl($scope, socket) {
  /* Controller logic */
}

Im Controller fügen wir Logik zum Senden und Empfangen von Nachrichten hinzu. Öffnen Sie js/public/controllers.js und ersetzen Sie den Inhalt durch Folgendes:

function AppCtrl($scope, socket) {

  // Socket listeners
  // ================

  socket.on('init', function (data) {
    $scope.name = data.name;
    $scope.users = data.users;
  });

  socket.on('send:message', function (message) {
    $scope.messages.push(message);
  });

  socket.on('change:name', function (data) {
    changeName(data.oldName, data.newName);
  });

  socket.on('user:join', function (data) {
    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + data.name + ' has joined.'
    });
    $scope.users.push(data.name);
  });

  // add a message to the conversation when a user disconnects or leaves the room
  socket.on('user:left', function (data) {
    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + data.name + ' has left.'
    });
    var i, user;
    for (i = 0; i < $scope.users.length; i++) {
      user = $scope.users[i];
      if (user === data.name) {
        $scope.users.splice(i, 1);
        break;
      }
    }
  });

  // Private helpers
  // ===============

  var changeName = function (oldName, newName) {
    // rename user in list of users
    var i;
    for (i = 0; i < $scope.users.length; i++) {
      if ($scope.users[i] === oldName) {
        $scope.users[i] = newName;
      }
    }

    $scope.messages.push({
      user: 'chatroom',
      text: 'User ' + oldName + ' is now known as ' + newName + '.'
    });
  }

  // Methods published to the scope
  // ==============================

  $scope.changeName = function () {
    socket.emit('change:name', {
      name: $scope.newName
    }, function (result) {
      if (!result) {
        alert('There was an error changing your name');
      } else {

        changeName($scope.name, $scope.newName);

        $scope.name = $scope.newName;
        $scope.newName = '';
      }
    });
  };

  $scope.sendMessage = function () {
    socket.emit('send:message', {
      message: $scope.message
    });

    // add the message to our model locally
    $scope.messages.push({
      user: $scope.name,
      text: $scope.message
    });

    // clear message box
    $scope.message = '';
  };
}

Diese App verfügt über nur eine Ansicht. Daher können wir die Weiterleitung aus public/js/app.js entfernen und vereinfachen:

// Declare app level module which depends on filters, and services
var app = angular.module('myApp', ['myApp.filters', 'myApp.directives']);

Server schreiben

Öffnen Sie routes/socket.js. Wir müssen ein -Objekt zur Verwaltung des Serverstatus definieren, damit die Nutzernamen eindeutig sind.

// Keep track of which names are used so that there are no duplicates
var userNames = (function () {
  var names = {};

  var claim = function (name) {
    if (!name || userNames[name]) {
      return false;
    } else {
      userNames[name] = true;
      return true;
    }
  };

  // find the lowest unused "guest" name and claim it
  var getGuestName = function () {
    var name,
      nextUserId = 1;

    do {
      name = 'Guest ' + nextUserId;
      nextUserId += 1;
    } while (!claim(name));

    return name;
  };

  // serialize claimed names as an array
  var get = function () {
    var res = [];
    for (user in userNames) {
      res.push(user);
    }

    return res;
  };

  var free = function (name) {
    if (userNames[name]) {
      delete userNames[name];
    }
  };

  return {
    claim: claim,
    free: free,
    get: get,
    getGuestName: getGuestName
  };
}());

Dies definiert im Grunde eine Reihe von Namen, aber mit APIs, die für die Domain eines Chat-Servers sinnvoller sind. Wir verbinden dies mit dem Socket des Servers, um auf die Aufrufe zu antworten, die unser Client ausführt:

// export function for listening to the socket
module.exports = function (socket) {
  var name = userNames.getGuestName();

  // send the new user their name and a list of users
  socket.emit('init', {
    name: name,
    users: userNames.get()
  });

  // notify other clients that a new user has joined
  socket.broadcast.emit('user:join', {
    name: name
  });

  // broadcast a user's message to other users
  socket.on('send:message', function (data) {
    socket.broadcast.emit('send:message', {
      user: name,
      text: data.message
    });
  });

  // validate a user's name change, and broadcast it on success
  socket.on('change:name', function (data, fn) {
    if (userNames.claim(data.name)) {
      var oldName = name;
      userNames.free(oldName);

      name = data.name;

      socket.broadcast.emit('change:name', {
        oldName: oldName,
        newName: name
      });

      fn(true);
    } else {
      fn(false);
    }
  });

  // clean up when a user leaves, and broadcast it to other users
  socket.on('disconnect', function () {
    socket.broadcast.emit('user:left', {
      name: name
    });
    userNames.free(name);
  });
};

Und damit die Bewerbung vollständig ist. Probieren Sie es aus, indem Sie node app.js ausführen. Die Anwendung sollte dank Socket.IO in Echtzeit aktualisiert werden.

Fazit

Es gibt noch viel mehr, das Sie zu dieser Instant Messaging-App hinzufügen könnten. Sie können zum Beispiel leere Nachrichten senden. Um dies auf Clientseite zu verhindern, können Sie ng-valid verwenden und den Server prüfen. Vielleicht könnte der Server den aktuellen Nachrichtenverlauf speichern, um neue Nutzer in der App zu erreichen.

Das Schreiben von AngularJS-Apps unter Verwendung anderer Bibliotheken ist einfach, wenn Sie wissen, wie Sie sie in einen Dienst einbinden und Angular darüber informieren, dass ein Modell geändert wurde. Als Nächstes werde ich AngularJS mit der beliebten Visualisierungsbibliothek D3.js verwenden.

Verweise

Angular Socket.IO Seed Fertige Instant Messaging-App AngularJS Express Socket.IO