Server-Sent Events в JavaScript (EventSource)
Изучите Server-Sent Events в JavaScript с API EventSource — односторонняя передача обновлений от сервера через HTTP с автоматическим переподключением и именованными событиями.
Server-Sent Events (SSE) — это стандартный способ отправки сервером непрерывного одностороннего потока текстовых обновлений в браузер через единственное долгоживущее HTTP-соединение. Вместо того чтобы клиент многократно опрашивал эндпоинт через fetch с вопросом «есть что-то новое?», соединение остаётся открытым, и сервер пишет сообщения всякий раз, когда ему есть что отправить. Браузер предоставляет доступ к этому механизму через встроенный интерфейс EventSource, поэтому для получения потока не нужны никакие внешние библиотеки.
SSE особенно хорош, когда обновления идут в одном направлении — от сервера к клиенту. Живые новостные ленты, уведомления, индикаторы прогресса сборки или загрузки, мониторинговые панели и котировки акций — всё это идеальные применения.
Получение потока с помощью EventSource
Открыть поток можно одной строкой. Вы создаёте EventSource, направленный на серверный эндпоинт, а затем добавляете обработчики для генерируемых событий.
const es = new EventSource('/events');
es.onmessage = (event) => {
console.log('New message:', event.data);
};
es.onopen = () => {
console.log('Connection opened');
};
es.onerror = (error) => {
console.log('Connection error:', error);
};Стоит отметить несколько вещей:
- Соединение открывается сразу после создания
EventSource. onmessageсрабатывает для каждого стандартного (безымянного) сообщения, которое отправляет сервер. Полезная нагрузка всегда является строкой string и доступна черезevent.data.onopenсрабатывает однократно при установке соединения (и снова после переподключения).onerrorсрабатывает при разрыве или сбое соединения. В отличие от неудачногоfetch, это не обязательно означает, что нужно сдаться — браузер обычно попытается переподключиться самостоятельно (подробнее об этом ниже).
Вот немного более полный пример, который разбирает JSON-полезную нагрузку и обновляет страницу:
const es = new EventSource('/notifications');
es.onmessage = (event) => {
const data = JSON.parse(event.data);
const li = document.createElement('li');
li.textContent = data.message;
document.querySelector('#feed').append(li);
};Формат передачи данных
Сервер отвечает с типом содержимого text/event-stream и пишет обычный UTF-8-текст в простом построчном формате. Каждое сообщение — это блок строк вида field: value, а пустая строка обозначает конец одного сообщения.
data: Hello there
data: First line
data: Second line
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
id: 42
data: This message has an id
retry: 10000
data: Reconnect after 10 seconds next timeРаспознаваемые поля:
data:— полезная нагрузка сообщения. Если сообщение содержит несколько строкdata:, они объединяются символом новой строки (\n) перед доставкой в видеevent.data.event:— необязательное имя события. Без него сообщение доставляется вonmessage.id:— необязательный идентификатор, который браузер запоминает для переподключения.retry:— задержка переподключения в миллисекундах.
Строки, начинающиеся с двоеточия (:), являются комментариями и игнорируются — серверы часто отправляют их в качестве пинг-сообщений для поддержания соединения.
Именованные события
По умолчанию каждое сообщение поступает в onmessage. Сервер может вместо этого пометить сообщение полем event:, а клиент будет слушать именно это имя через addEventListener. Это позволяет направлять разные виды обновлений к разным обработчикам через одно соединение.
Для потока такого вида:
event: priceUpdate
data: {"symbol":"ACME","price":42.10}
event: newsAlert
data: Markets closed early today
data: a plain default messageКлиент настраивается следующим образом:
const es = new EventSource('/market');
es.addEventListener('priceUpdate', (event) => {
const { symbol, price } = JSON.parse(event.data);
console.log(`${symbol} is now ${price}`);
});
es.addEventListener('newsAlert', (event) => {
console.log('News:', event.data);
});
// Messages with no `event:` field still land here.
es.onmessage = (event) => {
console.log('Default message:', event.data);
};Автоматическое переподключение
Именно эта возможность отличает SSE от самостоятельной реализации потоковой передачи через fetch. Если соединение разрывается — ненадёжная сеть, перезапуск сервера, таймаут прокси — браузер автоматически переподключается после небольшой задержки. Вам не нужно писать никаких циклов повторных попыток.
Чтобы возобновление было надёжным, браузер отслеживает последний полученный id:. При переподключении он отправляет это значение обратно в заголовке запроса Last-Event-ID, чтобы сервер мог продолжить поток именно с того места, где он остановился, а не воспроизводить всё заново.
GET /events HTTP/1.1
Last-Event-ID: 42
Accept: text/event-streamСервер также может настроить задержку перед следующим переподключением, отправив поле retry: (в миллисекундах):
retry: 5000
data: The browser will wait 5 seconds before reconnecting if droppedАвтоматическое переподключение происходит только пока соединение считается восстанавливаемым. Если сервер отвечает на переподключение HTTP-ошибкой, такой как 204 No Content или статусом 4xx, браузер считает поток завершённым и прекращает попытки.
Состояние соединения и закрытие
EventSource предоставляет своё текущее состояние через свойство readyState, которое соответствует трём константам:
EventSource.CONNECTING(0) — подключение или ожидание переподключения.EventSource.OPEN(1) — подключено и получает данные.EventSource.CLOSED(2) — закрыто и не будет переподключаться.
Когда поток больше не нужен, вызовите es.close(). Это немедленно закрывает соединение, и — что важно — браузер не будет переподключаться после этого.
const es = new EventSource('/events');
// Stop listening after 30 seconds.
setTimeout(() => {
es.close();
console.log('readyState is now', es.readyState); // 2 (CLOSED)
}, 30000);Кросс-доменные запросы и учётные данные
По умолчанию EventSource следует политике одного источника. Для потоковой передачи с другого источника сервер должен отправлять соответствующие заголовки CORS (например, Access-Control-Allow-Origin).
Файлы cookie не отправляются при кросс-доменных запросах, если вы явно не разрешите это. Передайте параметр withCredentials и убедитесь, что сервер разрешает учётные данные в своей конфигурации CORS:
const es = new EventSource('https://api.example.com/events', {
withCredentials: true,
});Замечание о серверной стороне
SSE — это в основном браузерная возможность, но сервер тоже должен участвовать. Независимо от того, какой язык вы используете, рецепт одинаков: ответить с Content-Type: text/event-stream, держать соединение открытым вместо завершения ответа и записывать отформатированные блоки событий по мере поступления данных.
Вот минимальный пример на Node.js для демонстрации структуры:
import http from 'node:http';
http.createServer((req, res) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
let id = 0;
const timer = setInterval(() => {
id += 1;
res.write(`id: ${id}\n`);
res.write(`data: The time is ${new Date().toISOString()}\n\n`);
}, 1000);
// Stop the timer when the client disconnects.
req.on('close', () => clearInterval(timer));
}).listen(3000);Каждый фрагмент заканчивается пустой строкой (\n\n) для завершения сообщения. Клиент, подключённый через new EventSource('http://localhost:3000'), получал бы одно обновление в секунду.
Ограничения
SSE передаёт только текст (UTF-8) — здесь нет типа бинарных фреймов, поэтому нельзя передавать сырые байты, такие как изображения или аудио, без их предварительного кодирования. Это также строго однонаправленно: для отправки данных на сервер по-прежнему нужен обычный запрос через fetch или XMLHttpRequest, либо полнодуплексное соединение WebSocket.
Ещё одно практическое ограничение: при использовании HTTP/1.1 браузеры ограничивают количество одновременных соединений к одному источнику примерно шестью. Поскольку каждый EventSource удерживает одно соединение открытым, открытие множества вкладок с одним и тем же сайтом может исчерпать этот лимит. При использовании HTTP/2 это гораздо менее актуально, поскольку многие потоки мультиплексируются в одно соединение.
SSE против WebSockets
SSE и WebSockets оба доставляют данные в реальном времени, но решают разные задачи.
| Server-Sent Events | WebSockets | |
|---|---|---|
| Направление | Одностороннее (сервер → клиент) | Двустороннее (полный дуплекс) |
| Протокол | Обычный HTTP | Обновление ws:// / wss:// |
| Данные | Только текст (UTF-8) | Текст и бинарные данные |
| Переподключение | Автоматическое, встроенное | Реализуйте самостоятельно |
| Лучше всего для | Ленты, уведомления, прогресс | Чат, игры, совместное редактирование |
Выбирайте SSE, когда браузеру нужно только слушать — уведомления, живые ленты, панели мониторинга, обновления прогресса. Автоматическое переподключение, транспорт на основе обычного HTTP и минимальный клиентский API делают его простейшим инструментом для этих задач. Выбирайте WebSockets, когда клиенту также нужно часто отправлять данные, когда нужны бинарные данные, или когда важна задержка на сообщение, как в чат-приложениях и многопользовательских играх.
Если ваши данные уже используют поток на основе промисов, сочетание обработчиков SSE с async/await для любых последующих запросов (например, получение полных деталей при поступлении лёгкого уведомления) делает код чистым и читаемым.