refactor 3-more into separate books

This commit is contained in:
Ilya Kantor 2015-02-27 13:21:58 +03:00
parent bd1d5e4305
commit 87639b2740
423 changed files with 9 additions and 9 deletions

View 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>

View 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>

View 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;
}