Я играл с Node.js и Express.js и случайно наткнулся на статью об отправленных сервером событиях. Отправленные сервером события — это спецификация W3C, которая описывает, как сервер может передавать события клиентам браузера. Все это с использованием стандартного протокола HTTP. Пару лет назад это была очень многообещающая спецификация, но затем появились веб-сокеты, и интерес к этой спецификации немного снизился. Однако это очень хороший, простой в использовании и легкий способ отправки обновлений с сервера на несколько подключенных клиентов. Код, особенно на стороне клиента, очень тривиален, и вы можете приступить к работе через пару минут.
Поэтому я решил немного углубиться в эту спецификацию, и в этой статье я покажу вам, как использовать отправленные сервером события в Angular.js. Я создам простой сервер в Node.js и Express.js, который будет отправлять события нашему веб-интерфейсу Angular.js.
В основном то, что мы собираемся сделать в этой статье, это две вещи:
- Создайте минимальное приложение Angular.js, которое показывает системную статистику
- Создайте бэкэнд для node.js и Express.js, который передает эту статистику в подключенные браузеры
После завершения это будет выглядеть примерно так:
Сборка приложения Angular.js
Я не буду вдаваться в подробности того, как работает Angular.js. Если вы читаете это, вы, вероятно, знаете основы Angular.js, поэтому я просто перейду к части событий, отправляемых сервером. Если мы хотим слушать отправленные сервером события (sse), нам нужно только включить небольшой фрагмент JavaScript:
var source = new EventSource('/stats');
source.addEventListener('message', handleCallback, false);
Это создаст слушателя, который автоматически пытается подключиться к / stats, используя стандартный HTTP-запрос. Если он потерпит неудачу или потеряет соединение, он немедленно попытается восстановить соединение без необходимости что-либо предпринимать. Когда сервер отправляет событие, функция handleCallback будет вызываться с полученными данными. В отличие от веб-сокетов мы можем получать только текстовые данные, используя sse. Это, однако, не должно быть слишком большой проблемой, так как в любом случае большая часть данных отправляется в виде строк JSON.
Итак, как нам это использовать, давайте начнем с рассмотрения угловых вещей:
// define the module we're working with
var app = angular.module('sse', []);
// define the ctrl
function statCtrl($scope) {
// the last received msg
$scope.msg = {};
// handles the callback from the received event
var handleCallback = function (msg) {
$scope.$apply(function () {
$scope.msg = JSON.parse(msg.data)
});
}
var source = new EventSource('/stats');
source.addEventListener('message', handleCallback, false);
}
Изменить: изменил $ scope. $ Apply, как предложено Джимом Хоскинсом (см. Первый комментарий)
Примечание: Это очень тривиальный пример. Обычно хорошей практикой является объединение этой конкретной функции события в службу, которую вы вводите в контроллер, поэтому контроллер содержит только логику приложения.
Как видите, мы создали один контроллер, который при запуске регистрирует слушателя и добавляет обратный вызов. В этом обратном вызове все, что мы делаем, это назначаем полученные данные переменной области видимости. Однако этого недостаточно для запуска обновлений в нашей модели. Поскольку события принимаются вне жизненного цикла Angular.js, мы должны сообщить Angular.js, что значение «msg» изменилось. Это делается с помощью $ scope. $ Apply. С этим фрагментом кода каждый раз, когда мы получаем событие, отправленное сервером, наша модель обновляется, и обновляются любые выражения представления.
Для полноты рассмотрим определение таблицы:
div class="container main" ng-controller="statCtrl">
<div class="row">
<div class="span10 offset1" style="text-align: center">
<h2>System details updated using server-sent events</h2>
</div>
</div>
<div class="row">
<div class="span8 offset2">
<table class="table table-striped">
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Hostname:</td>
<td>{{msg.hostname}}</td>
</tr>
<tr>
<td>Type:</td>
<td>{{msg.type}}</td>
</tr>
<tr>
<td>Platform:</td>
<td>{{msg.platform}}</td>
</tr>
<tr>
<td>Arch:</td>
<td>{{msg.arch}}</td>
</tr>
<tr>
<td>Release:</td>
<td>{{msg.arch}}</td>
</tr>
<tr>
<td>Uptime:</td>
<td>{{msg.arch}}</td>
</tr>
<tr>
<td>Load avg.:</td>
<td>{{msg.loadaverage}}</td>
</tr>
<tr>
<td>Total mem:</td>
<td>{{msg.totalmem}}</td>
</tr>
<tr>
<td>Free mem:</td>
<td>{{msg.freemem}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
Ничего особенного. Просто простая таблица (с использованием начальной загрузки для макета) с угловыми выражениями.
Теперь, когда мы увидели интерфейс, нам нужен сервер, который может отправлять события. В этом примере я использовал Node.js и Express.js, так как экспериментирую с этими двумя технологиями. Но, как вы увидите, реализация этого на любом другом HTTP-сервере будет тривиальной.
Сборка на стороне сервера с помощью node.js и express.js
В спецификации sse объясняется, как должен вести себя сервер, если он хочет поддерживать sse. Он должен держать HTTP-соединение открытым и отвечать конкретным ответом, чтобы браузер знал, что он может ожидать события, поступающие через это открытое соединение. Я перечислю полный app.js, использованный в этом примере, и выделю интересные части:
// most basic dependencies
var express = require('express')
, http = require('http')
, os = require('os')
, path = require('path');
// create the app
var app = express();
// configure everything, just basic setup
app.configure(function(){
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
});
// simple standard errorhandler
app.configure('development', function(){
app.use(express.errorHandler());
});
//---------------------------------------
// mini app
//---------------------------------------
var openConnections = [];
// simple route to register the clients
app.get('/stats', function(req, res) {
// set timeout as high as possible
req.socket.setTimeout(Infinity);
// send headers for event-stream connection
// see spec for more information
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
res.write('\n');
// push this res object to our global variable
openConnections.push(res);
// When the request is closed, e.g. the browser window
// is closed. We search through the open connections
// array and remove this connection.
req.on("close", function() {
var toRemove;
for (var j =0 ; j < openConnections.length ; j++) {
if (openConnections[j] == res) {
toRemove =j;
break;
}
}
openConnections.splice(j,1);
console.log(openConnections.length);
});
});
setInterval(function() {
// we walk through each connection
openConnections.forEach(function(resp) {
var d = new Date();
resp.write('id: ' + d.getMilliseconds() + '\n');
resp.write('data:' + createMsg() + '\n\n'); // Note the extra newline
});
}, 1000);
function createMsg() {
msg = {};
msg.hostname = os.hostname();
msg.type = os.type();
msg.platform = os.platform();
msg.arch = os.arch();
msg.release = os.release();
msg.uptime = os.uptime();
msg.loadaverage = os.loadavg();
msg.totalmem = os.totalmem();
msg.freemem = os.freemem();
return JSON.stringify(msg);
}
// startup everything
http.createServer(app).listen(app.get('port'), function(){
console.log("Express server listening on port " + app.get('port'));
})
Как вы, вероятно, можете видеть и читать из комментариев, ничего особенного не происходит. Мы создали маршрут «/ stats», который отвечает на запросы GET. Это конечная точка, которую вызывает наше приложение Angular.js, когда оно устанавливает слушателя. В этой функции вы можете видеть, что мы отвечаем определенным HTTP-заголовком. Это сообщает браузеру, что этот сервер будет использовать sse для отправки событий.
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
res.write('\n');
Вы также можете видеть, что мы храним массив открытых соединений. Мы используем это для отправки этих обновлений на все открытые соединения и для удаления соединений, когда они закрыты (в этом примере сделано очень примитивно):
// push this res object to our global variable
openConnections.push(res);
// When the request is closed, e.g. the browser window
// is closed. We search through the open connections
// array and remove this connection.
req.on("close", function() {
var toRemove;
for (var j =0 ; j < openConnections.length ; j++) {
if (openConnections[j] == res) {
toRemove =j;
break;
}
}
openConnections.splice(j,1);
console.log(openConnections.length);
});
Обновления отправляются в виде строки JSON в подключенные браузеры каждую секунду с помощью функции setInterval:
setInterval(function() {
// we walk through each connection
openConnections.forEach(function(resp) {
var d = new Date();
resp.write('id: ' + d.getMilliseconds() + '\n');
resp.write('data:' + createMsg() + '\n\n'); // Note the extra newline
});
}, 1000);
function createMsg() {
msg = {};
msg.hostname = os.hostname();
msg.type = os.type();
msg.platform = os.platform();
msg.arch = os.arch();
msg.release = os.release();
msg.uptime = os.uptime();
msg.loadaverage = os.loadavg();
msg.totalmem = os.totalmem();
msg.freemem = os.freemem();
return JSON.stringify(msg);
}
Вот и все. С помощью этой пары строк кода мы настроили сервер «отправленное сервером событие» и создали клиентское приложение, которое автоматически обновляет свой вид с помощью Angular.js.
