Написание приложения AngularJS с помощью Socket.IO

Введение

AngularJS — это великолепная среда JavaScript, которая обеспечивает двустороннюю привязку данных, простую в использовании и быструю, мощную систему директив, которая позволяет создавать повторно используемые пользовательские компоненты, а также многое другое. Socket.IO — это кроссбраузерная оболочка и полифилл для веб-сокетов, которые упрощают разработку приложений реального времени. Между прочим, эти двое неплохо работают вместе!

Ранее я писал о написании приложения AngularJS с помощью Express , но на этот раз я буду писать о том, как интегрировать Socket.IO для добавления функций реального времени в приложение AngularJS. В этом уроке я расскажу о написании приложения для обмена мгновенными сообщениями. Это основано на моем предыдущем руководстве (с использованием аналогичного стека node.js на сервере), поэтому я рекомендую сначала проверить это, если вы не знакомы с Node.js или Express.

Открыть демо-версию

Как всегда, готовый продукт можно получить на Github .

Предварительные условия

Для настройки и интеграции Socket.IO с Express требуется некоторый шаблон, поэтому я создал Angular Socket.IO Seed .

Для начала вы можете клонировать репозиторий angular-node-seed с Github:

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

или загрузите его в формате zip .

Получив начальное значение, вам нужно получить несколько зависимостей с помощью npm. Откройте терминал в каталоге с семенем и запустите:

npm install

Установив эти зависимости, вы можете запустить скелетное приложение:

node app.js

и просмотрите его в своем браузере по адресу http://localhost:3000 , чтобы убедиться, что начальное число работает должным образом.

Выбор функций приложения

Существует множество различных способов написания приложения для чата, поэтому давайте опишем минимальные функции, которые будут иметь наши. Будет только один чат, к которому будут принадлежать все пользователи. Пользователи могут выбирать и изменять свое имя, но имена должны быть уникальными. Сервер будет обеспечивать эту уникальность и сообщать, когда пользователи меняют свои имена. Клиент должен предоставить список сообщений и список пользователей, которые в данный момент находятся в чате.

Простой интерфейс

С помощью этой спецификации мы можем создать простой интерфейс с Jade, который предоставляет необходимые элементы пользовательского интерфейса. Откройте views/index.jade и добавьте это в block body :

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')

Откройте public/css/app.css и добавьте CSS для предоставления столбцов и переполнений:

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

Взаимодействие с Socket.IO

Хотя Socket.IO предоставляет переменную io в window , лучше инкапсулировать ее в систему внедрения зависимостей AngularJS. Итак, мы начнем с написания службы для переноса объекта socket , возвращаемого Socket.IO. Это здорово, потому что позже будет намного проще тестировать наш контроллер. Откройте public/js/services.js и замените содержимое на:

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

Обратите внимание, что мы оборачиваем каждый обратный вызов сокета в $scope.$apply . Это сообщает AngularJS, что ему необходимо проверить состояние приложения и обновить шаблоны, если произошли изменения после выполнения переданного ему обратного вызова. Внутренне $http работает таким же образом; после некоторых возвратов XHR он вызывает $scope.$apply , чтобы AngularJS мог соответствующим образом обновить свои представления.

Обратите внимание, что этот сервис не охватывает весь API Socket.IO (это оставлено в качестве упражнения для читателя ;P ). Тем не менее, он охватывает методы, используемые в этом руководстве, и должен указать вам правильное направление, если вы хотите его расширить. Я могу вернуться к написанию полной оболочки, но это выходит за рамки данного руководства.

Теперь внутри нашего контроллера мы можем запросить объект socket , как и в случае с $http :

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

Внутри контроллера добавим логику отправки и получения сообщений. Откройте js/public/controllers.js и замените содержимое следующим:

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 = '';
  };
}

В этом приложении будет только одно представление, поэтому мы можем удалить маршрутизацию из public/js/app.js и упростить ее до:

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

Написание сервера

Откройте routes/socket.js . Нам нужно определить объект для поддержания состояния сервера, чтобы имена пользователей были уникальными.

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

По сути, это определяет набор имен, но с API-интерфейсами, которые имеют больше смысла для домена чат-сервера. Давайте подключим это к сокету сервера, чтобы отвечать на вызовы, которые делает наш клиент:

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

И при этом приложение должно быть завершено. Попробуйте это, запустив node app.js Приложение должно обновляться в режиме реального времени благодаря Socket.IO.

Заключение

В это приложение для обмена мгновенными сообщениями можно добавить гораздо больше. Например, вы можете отправлять пустые сообщения. Вы можете использовать ng-valid , чтобы предотвратить это на стороне клиента и проверить на сервере. Возможно, сервер мог бы хранить недавнюю историю сообщений для того, чтобы новые пользователи могли присоединиться к приложению.

Написание приложений AngularJS, использующих другие библиотеки, не составит труда, если вы поймете, как обернуть их в сервис и уведомить Angular об изменении модели. Далее я планирую рассказать об использовании AngularJS с D3.js , популярной библиотекой визуализации.

Рекомендации

Angular Socket.IO Seed Готовое приложение для обмена мгновенными сообщениями AngularJS Express Socket.IO `