Ни 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
.
Создание сервера Socket.IO
Поскольку нам не нужны сложные веб-фреймворки, такие как 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);
});
});
|
Теперь вы должны запустить приложение и оставить его запущенным, прежде чем переходить к остальной части учебника.
Подготовка: Front-End
Нам, конечно, понадобится немного 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 позже, так как у нас еще нет библиотеки.
Создание библиотеки AngularJS Socket.IO
В этом разделе мы создадим библиотеку 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 = {};
|
Но:
- Мы должны будем использовать
Function.bind()
позже, чтобы получить доступ к соответствующему контексту дляthis
- Если мы используем сеттеры, мы можем проверить, чтобы убедиться, что установлены правильные значения — мы не хотим ставить
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 {
};
};
|
Мы положим все методы для службы в нем.
Метод on()
Этот метод прикрепит прослушиватель событий к объекту сокета, поэтому мы можем использовать любые данные, отправленные с сервера:
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 в них.
Метод off()
Этот метод удалит одного или всех слушателей события для данного события. Это поможет вам избежать утечек памяти и неожиданного поведения. Представьте, что вы используете 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);
}
},
|
Метод emit()
Это последний метод, который нам нужен. Как следует из названия, этот метод отправит данные на сервер:
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.
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 в наших одностраничных приложениях. Во второй части я покажу вам, как вы можете улучшить отзывчивость ваших приложений, используя эту комбинацию.