Статьи

Более отзывчивые одностраничные приложения с AngularJS & Socket.IO: создание библиотеки

Ни HTML, ни HTTP не были созданы для динамических веб-приложений. Мы в основном полагаемся на хаки, а не на хаки, чтобы дать нашим приложениям адаптивный пользовательский интерфейс. AngularJS снимает некоторые ограничения с HTML, что позволяет нам легче создавать и управлять кодом пользовательского интерфейса. Socket.IO , с другой стороны, помогает нам отправлять данные с сервера не только тогда, когда клиент запрашивает их, но и когда это необходимо серверу. В этой статье я покажу вам, как объединить эти два параметра, чтобы улучшить отзывчивость ваших одностраничных приложений.


В первой части этого руководства мы создадим повторно используемый сервис AngularJS для Socket.IO. Из-за этой повторно используемой части это будет немного сложнее, чем просто использование module.service() или module.factory() . Эти две функции являются просто синтаксическим сахаром поверх более низкоуровневого метода module.provider() , который мы будем использовать для предоставления некоторых параметров конфигурации. Если вы никогда ранее не использовали AngularJS, я настоятельно советую вам, по крайней мере, следовать официальному учебнику и некоторым учебникам здесь, на Tuts + .


Прежде чем мы начнем писать наш модуль AngularJS, нам нужен простой бэкэнд для тестирования. Если вы уже знакомы с Socket.IO, вы можете просто прокрутить вниз до конца этого раздела, скопировать внутренний источник и перейти к следующему, если нет — читать дальше.

Нам понадобится только socket.io . Вы можете установить его напрямую, используя команду npm например:

1
npm install socket.io

Или создайте файл package.json , поместите эту строку в раздел dependencies :

1
«socket.io»: «0.9.x»

И выполните команду npm install .

Поскольку нам не нужны сложные веб-фреймворки, такие как Express , мы можем создать сервер с помощью Socket.IO:

1
var io = require(‘socket.io’)(8080);

Это все, что вам нужно для настройки сервера Socket.IO. Если вы запустите свое приложение, вы увидите похожий вывод в консоли:

И вы должны иметь доступ к файлу socket.io.js в вашем браузере по адресу http: // localhost: 8080 / socket.io / socket.io.js :

Мы будем обрабатывать все входящие соединения в слушателе событий io.sockets объекта io.sockets :

1
2
3
io.sockets.on(‘connection’, function (socket) {
 
});

Атрибут socket переданный обратному вызову, является клиентом, который подключился, и мы можем прослушивать события в нем.

Теперь мы добавим базовый слушатель события в обратный вызов выше. Он отправит полученные данные обратно клиенту с помощью socket.emit() :

1
2
3
socket.on(‘echo’, function (data) {
       socket.emit(‘echo’, data);
   });

echo — это пользовательское имя события, которое мы будем использовать позже.

Мы также будем использовать подтверждения в нашей библиотеке. Эта функция позволяет передавать функцию в качестве третьего параметра метода socket.emit() . Эта функция может быть вызвана на сервере для отправки некоторых данных клиенту:

1
2
3
socket.on(‘echo-ack’, function (data, callback) {
       callback(data);
   });

Это позволяет вам отвечать клиенту, не требуя от него прослушивания каких-либо событий (что полезно, если вы хотите просто запросить некоторые данные с сервера).

Теперь наш тестовый сервер завершен. Код должен выглядеть следующим образом ( этот код вы должны скопировать, если пропустите этот раздел ):

01
02
03
04
05
06
07
08
09
10
11
var io = require(‘socket.io’)(8080);
 
io.sockets.on(‘connection’, function (socket) {
    socket.on(‘echo’, function (data) {
        socket.emit(‘echo’, data);
    });
 
    socket.on(‘echo-ack’, function (data, callback) {
        callback(data);
    });
});

Теперь вы должны запустить приложение и оставить его запущенным, прежде чем переходить к остальной части учебника.


Нам, конечно, понадобится немного HTML для тестирования нашей библиотеки. Мы должны включить AngularJS, socket.io.js из нашего socket.io.js , нашу библиотеку angular-socket.js и базовый контроллер AngularJS для запуска некоторых тестов. Контроллер будет встроен в <head> документа для упрощения рабочего процесса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
    <script src=»https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js»></script>
    <script src=»http://localhost:8080/socket.io/socket.io.js»></script>
    <script src=»angular-socket.js»></script>
 
    <script type=»application/javascript»>
         
    </script>
</head>
<body>
 
</body>
</html>

Это все, что нам нужно на данный момент, мы вернемся к пустому тегу script позже, так как у нас еще нет библиотеки.


В этом разделе мы создадим библиотеку angular-socket.js . Весь код должен быть вставлен в этот файл.

Начнем с создания модуля для нашей библиотеки:

1
var module = angular.module(‘socket.io’, []);

У нас нет никаких зависимостей, поэтому массив во втором аргументе angular.module() пуст, но не удаляйте его полностью, иначе вы получите ошибку $injector:nomod . Это происходит потому, что форма angular.module() с одним аргументом получает ссылку на уже существующий модуль, а не создает новый.

Поставщики являются одним из способов создания сервисов AngularJS . Синтаксис прост: первый аргумент — это имя службы (а не имя провайдера!), А второй — функция конструктора для провайдера:

1
2
3
module.provider(‘$socket’, $socketProvider() {
 
});

Чтобы сделать библиотеку многократно используемой, нам потребуется разрешить изменения в конфигурации Socket.IO. Сначала давайте определим две переменные, которые будут содержать URL для соединения и объекта конфигурации (код на этом шаге переходит к функции $socketProvider() ):

1
2
var ioUrl = »;
   var ioConfig = {};

Теперь, поскольку эти переменные недоступны вне функции $socketProvider() (они являются частными ), мы должны создать методы (сеттеры) для их изменения. Конечно, мы могли бы просто сделать их публичными :

1
2
this.ioUrl = »;
   this.ioConfig = {};

Но:

  1. Мы должны будем использовать Function.bind() позже, чтобы получить доступ к соответствующему контексту для this
  2. Если мы используем сеттеры, мы можем проверить, чтобы убедиться, что установлены правильные значения — мы не хотим ставить false качестве опции 'connect timeout'

Полный список параметров для клиента Socket.IO можно увидеть на их вики-сайте GitHub . Мы создадим установщик для каждого из них плюс один для URL. Все методы выглядят одинаково, поэтому я объясню код одного из них, а остальные приведу ниже.

Давайте определим первый метод:

1
this.setConnectionUrl = function setConnectionUrl(url) {

Следует проверить тип параметра, передаваемого в:

1
if (typeof url == ‘string’) {

Если это тот, который мы ожидали, установите опцию:

1
ioUrl = url;

Если нет, он должен TypeError :

1
2
3
4
} else {
           throw new TypeError(‘url must be of type string’);
       }
   };

Для остальных из них мы можем создать вспомогательную функцию, чтобы она оставалась СУХОЙ :

1
2
3
4
5
6
7
function setOption(name, value, type) {
       if (typeof value != type) {
           throw new TypeError(«‘»+ name +»‘ must be of type ‘»+ type + «‘»);
       }
 
       ioConfig[name] = value;
   }

Он просто выдает TypeError если тип неправильный, в противном случае устанавливает значение. Вот код для остальных опций:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
this.setResource = function setResource(value) {
       setOption(‘resource’, value, ‘string’);
   };
   this.setConnectTimeout = function setConnectTimeout(value) {
       setOption(‘connect timeout’, value, ‘number’);
   };
   this.setTryMultipleTransports = function setTryMultipleTransports(value) {
       setOption(‘try multiple transports’, value, ‘boolean’);
   };
   this.setReconnect = function setReconnect(value) {
       setOption(‘reconnect’, value, ‘boolean’);
   };
   this.setReconnectionDelay = function setReconnectionDelay(value) {
       setOption(‘reconnection delay’, value, ‘number’);
   };
   this.setReconnectionLimit = function setReconnectionLimit(value) {
       setOption(‘reconnection limit’, value, ‘number’);
   };
   this.setMaxReconnectionAttempts = function setMaxReconnectionAttempts(value) {
       setOption(‘max reconnection attempts’, value, ‘number’);
   };
   this.setSyncDisconnectOnUnload = function setSyncDisconnectOnUnload(value) {
       setOption(‘sync disconnect on unload’, value, ‘boolean’);
   };
   this.setAutoConnect = function setAutoConnect(value) {
       setOption(‘auto connect’, value, ‘boolean’);
   };
   this.setFlashPolicyPort = function setFlashPolicyPort(value) {
       setOption(‘flash policy port’, value, ‘number’)
   };
   this.setForceNewConnection = function setForceNewConnection(value) {
       setOption(‘force new connection’, value, ‘boolean’);
   };

Вы можете заменить его одним setOption() , но, кажется, проще набрать имя опции в случае верблюда, чем передавать его в виде строки с пробелами.

Эта функция создаст объект службы, который мы можем использовать позже (например, в контроллерах). Во-первых, давайте вызовем функцию io() для подключения к серверу Socket.IO:

1
2
this.$get = function $socketFactory($rootScope) {
       var socket = io(ioUrl, ioConfig);

Обратите внимание, что мы присваиваем функцию свойству $get объекта, созданного провайдером — это важно, поскольку AngularJS использует это свойство для его вызова. Мы также поместили $rootScope качестве его параметра. На этом этапе мы можем использовать внедрение зависимостей AngularJS для доступа к другим сервисам. Мы будем использовать его для распространения изменений в любых моделях в обратных вызовах Socket.IO.

Теперь функция должна вернуть объект:

1
2
3
4
return {
 
       };
   };

Мы положим все методы для службы в нем.

Этот метод прикрепит прослушиватель событий к объекту сокета, поэтому мы можем использовать любые данные, отправленные с сервера:

1
on: function on(event, callback) {

Мы будем использовать Socket.IO socket.on() для присоединения нашего обратного вызова и вызова его в методе $scope.$apply() socket.on() AngularJS. Это очень важно, потому что модели могут быть изменены только внутри него:

1
socket.on(event, function () {

Сначала мы должны скопировать аргументы во временную переменную, чтобы мы могли использовать их позже. Аргументы, конечно, все, что сервер отправил нам:

1
var args = arguments;

Затем мы можем вызвать наш обратный вызов, используя Function.apply() чтобы передать ему аргументы:

1
2
3
4
5
$rootScope.$apply(function () {
                       callback.apply(socket, args);
                   });
               });
           },

Когда $rootScope.$apply() событий socket вызывает функцию слушателя, он использует $rootScope.$apply() для вызова обратного вызова, предоставленного в качестве второго аргумента метода .on() . Таким образом, вы можете написать своих слушателей событий, как и для любого другого приложения, использующего Socket.IO, но вы можете изменять модели AngularJS в них.

Этот метод удалит одного или всех слушателей события для данного события. Это поможет вам избежать утечек памяти и неожиданного поведения. Представьте, что вы используете ngRoute и подключаете несколько слушателей к каждому контроллеру. Если пользователь переходит к другому представлению, ваш контроллер уничтожается, но прослушиватель событий остается подключенным. После нескольких переходов у нас будет утечка памяти.

1
off: function off(event, callback) {

Нам нужно только проверить, был ли предоставлен callback и вызвать socket.removeListener() или socket.removeAllListeners() :

1
2
3
4
5
6
if (typeof callback == ‘function’) {
                   socket.removeListener(event, callback);
               } else {
                   socket.removeAllListeners(event);
               }
           },

Это последний метод, который нам нужен. Как следует из названия, этот метод отправит данные на сервер:

1
emit: function emit(event, data, callback) {

Так как Socket.IO поддерживает подтверждения, мы проверим, был ли предоставлен callback . Если это так, мы будем использовать тот же шаблон, что и в методе on() для вызова обратного вызова внутри $scope.$apply() :

1
2
3
4
5
6
7
8
if (typeof callback == ‘function’) {
                   socket.emit(event, data, function () {
                       var args = arguments;
 
                       $rootScope.$apply(function () {
                           callback.apply(socket, args);
                       });
                   });

Если callback нет, мы можем просто вызвать socket.emit() :

1
2
3
4
} else {
                   socket.emit(event, data);
               }
           }

Чтобы протестировать библиотеку, мы создадим простую форму, которая отправит некоторые данные на сервер и отобразит ответ. Весь код JavaScript в этом разделе должен идти в <script> в <head> вашего документа, а весь HTML-код помещается в его <body> .

Сначала мы должны создать модуль для нашего приложения:

1
var app = angular.module(‘example’, [ ‘socket.io’ ]);

Обратите внимание, что 'socket.io' в массиве во втором параметре сообщает AngularJS, что этот модуль зависит от нашей библиотеки Socket.IO.

Поскольку мы будем работать из статического HTML-файла, мы должны указать URL-адрес подключения для Socket.IO. Мы можем сделать это используя метод config() модуля:

1
2
3
app.config(function ($socketProvider) {
    $socketProvider.setConnectionUrl(‘http://localhost:8080’);
});

Как видите, наш $socketProvider автоматически вводится AngularJS.

Контроллер будет отвечать за всю логику приложения (приложение маленькое, поэтому нам нужен только один):

1
app.controller(‘Ctrl’, function Ctrl($scope, $socket) {

$scope — это объект, который содержит все модели контроллера, это база двунаправленной привязки данных AngularJS. $socket — это наш сервис Socket.IO.

Сначала мы создадим прослушиватель для события 'echo' которое будет генерироваться нашим тестовым сервером:

1
2
3
$socket.on(‘echo’, function (data) {
       $scope.serverResponse = data;
   });

Мы будем отображать $scope.serverResponse позже, в HTML, используя выражения AngularJS.

Теперь также будут две функции, которые будут отправлять данные — одна с использованием базового метода emit() и одна с использованием emit() с подтверждением обратного вызова:

01
02
03
04
05
06
07
08
09
10
11
12
$scope.emitBasic = function emitBasic() {
        $socket.emit(‘echo’, $scope.dataToSend);
        $scope.dataToSend = »;
    };
 
    $scope.emitACK = function emitACK() {
        $socket.emit(‘echo-ack’, $scope.dataToSend, function (data) {
            $scope.serverResponseACK = data;
        });
        $scope.dataToSend = »;
    };
});

Мы должны определить их как методы $scope чтобы мы могли вызывать их из директивы ngClick в HTML.

Вот где сияет AngularJS — мы можем использовать стандартный HTML с некоторыми пользовательскими атрибутами, чтобы связать все вместе.

Давайте начнем с определения основного модуля с помощью директивы ngApp . Поместите этот атрибут в <body> вашего документа:

1
<body ng-app=»example»>

Это говорит AngularJS, что он должен загрузить ваше приложение с помощью example модуля.

После этого мы можем создать базовую форму для отправки данных на сервер:

1
2
3
4
5
6
7
<div ng-controller=»Ctrl»>
       <input ng-model=»dataToSend»>
       <button ng-click=»emitBasic()»>Send</button>
       <button ng-click=»emitACK()»>Send (ACK)</button>
       <div>Server Response: {{ serverResponse }}</div>
       <div>Server Response (ACK): {{ serverResponseACK }}</div>
   </div>

Мы использовали несколько пользовательских атрибутов и директив AngularJS:

  • ng-controller — привязывает указанный контроллер к этому элементу, позволяя использовать значения из его области
  • ng-model — создает двунаправленную привязку данных между элементом и заданным свойством области действия (модель), что позволяет получать значения из этого элемента, а также изменять его внутри контроллера
  • ng-click — присоединяет прослушиватель события click который выполняет указанное выражение (подробнее о выражениях AngularJS )

Двойные фигурные скобки также являются выражениями AngularJS, они будут оцениваться (не волнуйтесь, не используя JavaScript eval() ), и их значение будет вставлено туда.

Если вы все сделали правильно, вы сможете отправлять данные на сервер, нажимая кнопки и видеть ответ в соответствующих тегах <div> .


В этой первой части руководства мы создали библиотеку Socket.IO для AngularJS, которая позволит нам использовать преимущества WebSockets в наших одностраничных приложениях. Во второй части я покажу вам, как вы можете улучшить отзывчивость ваших приложений, используя эту комбинацию.