AngularJS-Anwendung mit Socket.IO schreiben

Brian Ford
Brian Ford

Einführung

AngularJS ist ein hervorragendes JavaScript-Framework, das eine benutzerfreundliche und schnelle Zwei-Wege-Datenbindung, ein leistungsstarkes Direktivsystem zum Erstellen wiederverwendbarer benutzerdefinierter Komponenten und vieles mehr bietet. Socket.IO ist ein browserübergreifender Wrapper und eine Polyfill für WebSockets, die die Entwicklung von Echtzeitanwendungen zum Kinderspiel macht. Übrigens funktionieren die beiden Tools ziemlich gut zusammen.

Ich habe bereits darüber geschrieben, wie Sie eine AngularJS-App mit Express erstellen. Dieses Mal geht es darum, wie Sie Socket.IO einbinden, um einer AngularJS-Anwendung Echtzeitfunktionen hinzuzufügen. In dieser Anleitung zeige ich Ihnen, wie Sie eine Instant-Messaging-App erstellen. Diese Anleitung baut auf meinem vorherigen Tutorial auf (mit einem ähnlichen Node.js-Stack auf dem Server). Wenn Sie mit Node.js oder Express nicht vertraut sind, sollten Sie sich dieses Tutorial zuerst ansehen.

Demo öffnen

Wie immer können Sie das fertige Produkt auf GitHub herunterladen.

Vorbereitung

Es ist ein bisschen Boilerplate erforderlich, um Socket.IO einzurichten und in Express zu integrieren. Deshalb habe ich den Angular Socket.IO-Seed erstellt.

Sie können entweder das Repository „angular-node-seed“ von GitHub klonen:

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 einige Abhängigkeiten mit npm herunterladen. Öffnen Sie ein Terminal im Verzeichnis mit dem Seed und führen Sie Folgendes aus:

npm install

Nachdem Sie diese Abhängigkeiten installiert haben, können Sie die Skeletal App ausführen:

node app.js

und sehen Sie sich das Bild in Ihrem Browser unter http://localhost:3000 an, um sicherzustellen, dass der Seed wie erwartet funktioniert.

Entscheidung über App-Funktionen

Es gibt viele verschiedene Möglichkeiten, eine Chat-Anwendung zu schreiben. Beschreiben wir daher die minimalen Funktionen, die unsere haben wird. Es gibt nur einen Chatroom, zu dem alle Nutzer gehören. Nutzer können ihren Namen auswählen und ändern. Die Namen dürfen jedoch nicht identisch sein. Der Server erzwingt diese Eindeutigkeit und meldet, wenn Nutzer ihre Namen ändern. Der Client sollte eine Liste der Nachrichten und eine Liste der Nutzer anzeigen, die sich derzeit im Chatroom befinden.

Ein einfaches Frontend

Mit dieser Spezifikation können wir ein einfaches Front-End mit Jade 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 das CSS für Spalten und Überlauf hinzu:

/* 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

Socket.IO stellt zwar eine io-Variable auf der window bereit, es ist jedoch besser, sie im Dependency Injection-System von AngularJS zu kapseln. Wir beginnen also damit, einen Dienst zu schreiben, der das von Socket.IO zurückgegebene socket-Objekt umschließt. Das ist super, weil es später viel einfacher ist, unseren Controller zu testen. Öffnen Sie public/js/services.js und ersetzen Sie den Inhalt durch Folgendes:

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 einschließen. Dadurch wird AngularJS mitgeteilt, dass es den Status der Anwendung prüfen und die Vorlagen aktualisieren muss, wenn sich nach dem Ausführen des übergebenen Rückrufs etwas geändert hat. Intern funktioniert $http auf die gleiche Weise. Nach einigen XHR-Rückgaben wird $scope.$apply aufgerufen, damit AngularJS seine Ansichten entsprechend aktualisieren kann.

Dieser Dienst umschließt nicht die gesamte Socket.IO API (das bleibt dem Leser als Übung ;P). Er deckt jedoch die in dieser Anleitung verwendeten Methoden ab und sollte Ihnen eine gute Orientierung bieten, wenn Sie mehr darüber erfahren möchten. Ich werde möglicherweise noch einmal einen vollständigen Wrapper schreiben, aber das würde den Rahmen dieses Tutorials sprengen.

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

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

Fügen wir dem Controller die 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 Anwendung hat nur eine Ansicht, sodass wir das Routing aus public/js/app.js entfernen und es so vereinfachen können:

// 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 zum Speichern des Serverstatus definieren, damit 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
  };
}());

Damit wird im Grunde eine Reihe von Namen definiert, aber mit APIs, die für die Domain eines Chatservers sinnvoller sind. Verbinden wir das mit dem Socket des Servers, um auf die Aufrufe unseres Clients zu reagieren:

// 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);
  });
};

Damit sollte die Anwendung fertig sein. Führen Sie node app.js aus, um es zu testen. Dank Socket.IO sollte die Anwendung in Echtzeit aktualisiert werden.

Fazit

Sie können dieser Messaging-App noch viel mehr hinzufügen. Sie können beispielsweise auch leere Nachrichten senden. Sie können ng-valid verwenden, um dies auf der Clientseite zu verhindern, und eine Prüfung auf dem Server. Vielleicht könnte der Server einen aktuellen Nachrichtenverlauf speichern, damit neue Nutzer, die der App beitreten, die Unterhaltungen verfolgen können.

Das Erstellen von AngularJS-Apps, die andere Bibliotheken verwenden, ist ganz einfach, sobald Sie wissen, wie Sie sie in einen Dienst einbetten und Angular benachrichtigen, dass sich ein Modell geändert hat. Als Nächstes möchte ich die Verwendung von AngularJS mit D3.js, der beliebten Visualisierungsbibliothek, behandeln.

Verweise

Angular Socket.IO Seed Fertiggestellte Instant-Messaging-App AngularJS Express Socket.IO`