refactor 3-more into separate books
This commit is contained in:
parent
bd1d5e4305
commit
87639b2740
423 changed files with 9 additions and 9 deletions
295
4-ajax/11-server-sent-events/article.md
Normal file
295
4-ajax/11-server-sent-events/article.md
Normal file
|
@ -0,0 +1,295 @@
|
|||
# Server Side Events -- события с сервера
|
||||
|
||||
Сразу заметим, что на текущий момент этот способ поддерживают все современные браузеры, кроме IE.
|
||||
|
||||
Современный стандарт [Server-Sent Events](https://html.spec.whatwg.org/multipage/comms.html#the-eventsource-interface) позволяет браузеру создавать специальный объект `EventSource`, который сам обеспечивает соединение с сервером, делает пересоединение в случае обрыва и генерирует события при поступлении данных.
|
||||
|
||||
Он, по дизайну, может меньше, чем WebSocket'ы.
|
||||
|
||||
С другой стороны, Server Side Events проще в реализации, работают по обычному протоколу HTTP и сразу поддерживают ряд возможностей, которые для WebSocket ещё надо реализовать.
|
||||
|
||||
Поэтому в тех случаях, когда нужна преимущественно односторонняя передача данных от сервера к браузеру, они могут быть удачным выбором.
|
||||
|
||||
[cut]
|
||||
|
||||
## Получение сообщений
|
||||
|
||||
При создании объекта `new EventSource(src)` браузер автоматически подключается к адресу `src` и начинает получать с него события:
|
||||
|
||||
```js
|
||||
var eventSource = new EventSource("/events/subscribe");
|
||||
|
||||
eventSource.onmessage = function(e) {
|
||||
console.log("Пришло сообщение: " + e.data);
|
||||
};
|
||||
```
|
||||
|
||||
Чтобы соединение успешно открылось, сервер должен ответить с заголовком `Content-Type: text/event-stream`, а затем оставить соединение висящим и писать в него сообщения в специальном формате:
|
||||
|
||||
```
|
||||
data: Сообщение 1
|
||||
|
||||
data: Сообщение 2
|
||||
|
||||
data: Сообщение 3
|
||||
data: из двух строк
|
||||
```
|
||||
|
||||
<ul>
|
||||
<li>Каждое сообщение пишется после `data:`. Если после двоеточия есть пробел, то он игнорируется.</li>
|
||||
<li>Сообщения разделяются двумя строками `\n\n`.</li>
|
||||
<li>Если нужно переслать перевод строки, то сообщение разделяется. Каждая следующая строка пересылается отдельным `data:`.
|
||||
|
||||
В частности, две последние строки в примере выше составляют одно сообщение: `"Сообщение 3\nиз двух строк"`.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Здесь все очень просто и удобно, кроме разделения сообщения при переводе строки. Но, если подумать -- это не так уж страшно: на практике сложные сообщения обычно передаются в формате JSON. А перевод строки в нём кодируется как `\n`.
|
||||
|
||||
Соответственно, многострочные данные будут пересылаться так:
|
||||
|
||||
```
|
||||
data: {"user":"Вася","message":"Сообщение 3\n из двух строк"}
|
||||
```
|
||||
|
||||
...То есть, строка `data:` будет одна, и накаких проблем с разделением сообщения нет.
|
||||
|
||||
## Восстановление соединения
|
||||
|
||||
При создании объекта браузер автоматически подключается к серверу, а при обрыве -- пытается его возобновить.
|
||||
|
||||
Это очень удобно, никакой другой транспорт не обладает такой встроенной способностью.
|
||||
|
||||
[smart header="Как серверу полностью закрыть соединение?"]
|
||||
При любом закрытии соединения, в том числе если сервер ответит на запрос и закроет соединение сам -- браузер через короткое время повторит свой запрос.
|
||||
|
||||
Есть лишь два способа, которыми сервер может "отшить" надоедливый `EventSource`:
|
||||
<ul>
|
||||
<li>Ответить со статусом не 200.</li>
|
||||
<li>Ответить с `Content-Type`, не совпадающим с `text/event-stream`.</li>
|
||||
</ul>
|
||||
[/smart]
|
||||
|
||||
Между попытками возобновить соединение будет пауза, начальное значение которой зависит от браузера (1-3 секунды) и может быть изменено сервером через указание `retry:` в ответе:
|
||||
|
||||
```js
|
||||
retry: 15000
|
||||
data: Поставлена задержка 15 секунд
|
||||
```
|
||||
|
||||
Браузер, со своей стороны, может закрыть соединение вызовом `close()`:
|
||||
|
||||
```js
|
||||
var eventSource = new EventSource(...);
|
||||
|
||||
eventSource.close();
|
||||
```
|
||||
|
||||
При этом дальнейших попыток соединения не будет. Открыть обратно этот объект тоже нельзя, можно создать новый `EventSource`.
|
||||
|
||||
### Идентификатор id
|
||||
|
||||
Для того, чтобы продолжить получение событий с места разрыва, стандарт предусматривает идентификацию событий через `id`.
|
||||
|
||||
Сервер может указать его в ответе:
|
||||
|
||||
```
|
||||
data: Сообщение 1
|
||||
id: 1
|
||||
|
||||
data: Сообщение 2
|
||||
id: 2
|
||||
|
||||
data: Сообщение 3
|
||||
data: из двух строк
|
||||
id: 3
|
||||
```
|
||||
|
||||
При получении `id:` браузер:
|
||||
<ul>
|
||||
<li>Устанавливает свойство `eventSource.lastEventId` в его значение.</li>
|
||||
<li>При пересоединении пошлёт заголовок `Last-Event-ID` с этим `id`, так что сервер сможет переслать последующие, пропущенные, сообщения.</li>
|
||||
</ul>
|
||||
|
||||
Обратим внимание: `id` шлётся *не перед сообщением, а после него*, чтобы обновление `lastEventId` произошло, когда браузер всё уже точно получил.
|
||||
|
||||
## Статус соединения readyState
|
||||
|
||||
У объекта `EventSource` есть свойство `readyState`, которое содержит одно из значений (выдержка из стандарта):
|
||||
|
||||
```js
|
||||
const unsigned short CONNECTING = 0; // в процессе (пере-)соединения
|
||||
const unsigned short OPEN = 1; // соединение установлено
|
||||
const unsigned short CLOSED = 2; // соединение закрыто
|
||||
```
|
||||
|
||||
При создании объекта и при разрыве оно автоматически равно `CONNECTING`.
|
||||
|
||||
## События
|
||||
|
||||
Событий всего три:
|
||||
<ul>
|
||||
<li>`onmessage` -- пришло сообщение, доступно как `event.data`</li>
|
||||
<li>`onopen` -- при успешном установлении соединения</li>
|
||||
<li>`onerror` -- при ошибке соединения.</li>
|
||||
</ul>
|
||||
|
||||
Например:
|
||||
|
||||
```js
|
||||
var eventSource = new EventSource('digits');
|
||||
|
||||
eventSource.onopen = function(e) {
|
||||
console.log("Соединение открыто");
|
||||
};
|
||||
|
||||
eventSource.onerror = function(e) {
|
||||
if (this.readyState == EventSource.CONNECTING) {
|
||||
console.log("Соединение порвалось, пересоединяемся...");
|
||||
} else {
|
||||
console.log("Ошибка, состояние: " + this.readyState);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onmessage = function(e) {
|
||||
console.log("Пришли данные: " + e.data);
|
||||
};
|
||||
```
|
||||
|
||||
## Своё имя события: event
|
||||
|
||||
По умолчанию на события срабатывает обработчик `onmessage`, но можно сделать и свои события. Для этого сервер должен указать перед событием его имя после `event:`.
|
||||
|
||||
Например:
|
||||
|
||||
```
|
||||
event: join
|
||||
data: Вася
|
||||
|
||||
data: Привет
|
||||
|
||||
event: leave
|
||||
data: Вася
|
||||
```
|
||||
|
||||
Сообщение по умолчанию имеет имя `message`.
|
||||
|
||||
**Для обработки своих имён событий необходимо ставить обработчик при помощи `addEventListener`.**
|
||||
|
||||
Пример кода для обработки:
|
||||
|
||||
```js
|
||||
eventSource.addEventListener('join', function(e) {
|
||||
alert('Пришёл ' + e.data);
|
||||
});
|
||||
|
||||
eventSource.addEventListener('message', function(e) {
|
||||
alert('Сообщение ' + e.data);
|
||||
});
|
||||
|
||||
eventSource.addEventListener('leave', function(e) {
|
||||
alert('Ушёл ' + e.data);
|
||||
});
|
||||
```
|
||||
|
||||
## Демо
|
||||
|
||||
В примере ниже сервер посылает в соединение числа от 1 до 3, а затем -- событие `bye` и закрывает соединение. Браузер автоматически откроет его заново.
|
||||
|
||||
[codetabs src="eventsource"]
|
||||
|
||||
## Кросс-доменность
|
||||
|
||||
`EventSource` поддерживает кросс-доменные запросы, аналогично `XMLHttpRequest`. Для этого у конструктора есть второй аргумент -- объект, который нужно передать так:
|
||||
|
||||
```js
|
||||
var source = new EventSource("http://pupkin.ru/stream", { withCredentials: true });
|
||||
```
|
||||
|
||||
Второй аргумент сделан объектом с расчётом на будущее. Пока что никаких других свойств там не поддерживается, только `withCredentials`.
|
||||
|
||||
Сервер при этом получит заголовок `Origin` с доменом запроса и должен ответить с заголовком `Access-Control-Allow-Origin` (и `Access-Control-Allow-Credentials`, если стоит `withCredentials`), в точности как в главе [](/xhr-crossdomain).
|
||||
|
||||
При кросс-доменных запросах у событий `event` также появится дополнительное свойство `origin`, содержащее адрес источника, откуда пришли данные. Его можно использовать для дополнительной проверки со стороны браузера:
|
||||
|
||||
```js
|
||||
eventSource.addEventListener('message', function(e) {
|
||||
if (e.origin != 'http://javascript.ru') return;
|
||||
alert('Сообщение ' + e.data);
|
||||
});
|
||||
```
|
||||
|
||||
## Итого
|
||||
|
||||
Объект `EventSource` предназначен для передачи текстовых сообщений с сервера, используя обычный протокол HTTP.
|
||||
|
||||
Он предлагает не только передачу сообщений, но и встроенную поддержку важных вспомогательных функций:
|
||||
|
||||
<ul>
|
||||
<li>События `event`.</li>
|
||||
<li>Автоматическое пересоединение, с настраиваемой задержкой `retry`.</li>
|
||||
<li>Проверка текущего состояния подключения по `readyState`.</li>
|
||||
<li>Идентификаторы сообщений `id` для точного возобновления потока данных, последний полученный идентификатор передаётся в заголовке `Last-Event-ID`.</li>
|
||||
<li>Кросс-доменность CORS.</li>
|
||||
</ul>
|
||||
|
||||
Этот набор функций делает EventSource достойной альтернативой WebSocket, которые хоть и потенциально мощнее, но требуют реализации всех этих функций на клиенте и сервере, поверх протокола.
|
||||
|
||||
Поддержка -- все браузеры, кроме IE.
|
||||
|
||||
<ul>
|
||||
<li>Синтаксис:
|
||||
```js
|
||||
var source = new EventSource(src[, credentials]); // src - адрес с любого домена
|
||||
```
|
||||
|
||||
Второй необязательный аргумент, если указан в виде `{ withCredentials: true }`, инициирует отправку Cookie и данных авторизации при кросс-доменных запросах.
|
||||
|
||||
Безопасность при кросс-доменных запросах обеспечивается аналогично `XMLHttpRequest`.
|
||||
</li>
|
||||
<li>Свойства объекта:
|
||||
<dl>
|
||||
<dt>`readyState`</dt>
|
||||
<dd>Текущее состояние соединения, одно из `EventSource.CLOSING (=0)`, `EventSource.OPEN (=1)` или `EventSource.CLOSED (=2)`.</dd>
|
||||
<dt>`lastEventId`</dt>
|
||||
<dd>Последнее полученное `id`, если есть. При возобновлении соединения браузер указывает это значение в заголовке `Last-Event-ID`.</dd>
|
||||
<dt>`url`, `withCredentials`</dt>
|
||||
<dd>Параметры, переданные при создании объекта. Менять их нельзя.</dd>
|
||||
<dt>
|
||||
</dl>
|
||||
</li>
|
||||
<li>Методы:
|
||||
|
||||
<dl>
|
||||
<dt>`close()`</dt>
|
||||
<dd>Закрывает соединение.</dd>
|
||||
</dl>
|
||||
</li>
|
||||
<li>События:
|
||||
|
||||
<dl>
|
||||
<dt>`onmessage`</dt>
|
||||
<dd>При сообщении, данные -- в `event.data`.</dd>
|
||||
<dt>`onopen`</dt>
|
||||
<dd>При установлении соединения.</dd>
|
||||
<dt>`onerror`</dt>
|
||||
<dd>При ошибке, в том числе -- закрытии соединения по инициативе сервера.</dd>
|
||||
</dl>
|
||||
|
||||
Эти события можно ставить напрямую через свойство: `source.onmessage = ...`.
|
||||
|
||||
Если сервер присылает имя события в `event:`, то такие события нужно обрабатывать через `addEventListener`.
|
||||
</li>
|
||||
<li>Формат ответа сервера:
|
||||
|
||||
Сервер присылает пустые строки, либо строки, начинающиеся с:
|
||||
<ul>
|
||||
<li>`date:` -- сообщение, несколько таких строк подряд склеиваются и образуют одно сообщение.</li>
|
||||
<li>`id:` -- обновляет `lastEventId`.</li>
|
||||
<li>`retry:` -- указывает паузу между пересоединениями, в миллисекундах. JavaScript не может указать это значение, только сервер.</li>
|
||||
<li>`event:` -- имя события, должен быть перед `date:`.</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
61
4-ajax/11-server-sent-events/eventsource.view/index.html
Normal file
61
4-ajax/11-server-sent-events/eventsource.view/index.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<script>
|
||||
|
||||
var eventSource;
|
||||
|
||||
function start() { // при нажатии на Старт
|
||||
|
||||
if (!window.EventSource) {
|
||||
alert('В этом браузере нет поддержки EventSource.');
|
||||
return;
|
||||
}
|
||||
|
||||
eventSource = new EventSource('digits');
|
||||
|
||||
eventSource.onopen = function(e) {
|
||||
log("Соединение открыто");
|
||||
};
|
||||
|
||||
eventSource.onerror = function(e) {
|
||||
if (this.readyState == EventSource.CONNECTING) {
|
||||
log("Соединение порвалось, пересоединяемся...");
|
||||
} else {
|
||||
log("Ошибка, состояние: " + this.readyState);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.addEventListener('bye', function(e) {
|
||||
log("Bye: " + e.data);
|
||||
}, false);
|
||||
|
||||
eventSource.onmessage = function(e) {
|
||||
console.log(e);
|
||||
log(e.data);
|
||||
};
|
||||
}
|
||||
|
||||
function stop() { // при нажатии на Стоп
|
||||
eventSource.close();
|
||||
log("Соединение завершено");
|
||||
}
|
||||
|
||||
function log(msg) {
|
||||
logElem.innerHTML += msg + "<br>";
|
||||
}
|
||||
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<button onclick="start()">Старт</button>
|
||||
<button onclick="stop()">Стоп</button>
|
||||
|
||||
Нажмите "Старт" для начала.
|
||||
<div id="logElem"></div>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
54
4-ajax/11-server-sent-events/eventsource.view/server.js
Normal file
54
4-ajax/11-server-sent-events/eventsource.view/server.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
var http = require('http');
|
||||
var url = require('url');
|
||||
var querystring = require('querystring');
|
||||
|
||||
var fileServer = new (require('node-static')).Server('.');
|
||||
|
||||
function onDigits(req, res) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
'Cache-Control': 'no-cache'
|
||||
});
|
||||
|
||||
var i=0;
|
||||
|
||||
var timer = setInterval(write, 1000);
|
||||
write();
|
||||
|
||||
function write() {
|
||||
i++;
|
||||
|
||||
if (i == 4) {
|
||||
res.write('event: bye\ndata: до свидания\n\n');
|
||||
clearInterval(timer);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.write('data: ' + i + '\n\n');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function accept(req, res) {
|
||||
|
||||
if (req.url == '/digits') {
|
||||
onDigits(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
// всё остальное -- статика
|
||||
fileServer.serve(req, res);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ----- запуск accept как сервера из консоли или как модуля ------
|
||||
|
||||
if (!module.parent) {
|
||||
http.createServer(accept).listen(8080);
|
||||
} else {
|
||||
exports.accept = accept;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue