Я играл с 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.