W3docs

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 EventsWebSockets
НаправлениеОдностороннее (сервер → клиент)Двустороннее (полный дуплекс)
ПротоколОбычный HTTPОбновление ws:// / wss://
ДанныеТолько текст (UTF-8)Текст и бинарные данные
ПереподключениеАвтоматическое, встроенноеРеализуйте самостоятельно
Лучше всего дляЛенты, уведомления, прогрессЧат, игры, совместное редактирование

Выбирайте SSE, когда браузеру нужно только слушать — уведомления, живые ленты, панели мониторинга, обновления прогресса. Автоматическое переподключение, транспорт на основе обычного HTTP и минимальный клиентский API делают его простейшим инструментом для этих задач. Выбирайте WebSockets, когда клиенту также нужно часто отправлять данные, когда нужны бинарные данные, или когда важна задержка на сообщение, как в чат-приложениях и многопользовательских играх.

Информация

Если ваши данные уже используют поток на основе промисов, сочетание обработчиков SSE с async/await для любых последующих запросов (например, получение полных деталей при поступлении лёгкого уведомления) делает код чистым и читаемым.

Проверьте свои знания

Практика
Server-Sent Events обеспечивают коммуникацию в каком направлении?
Server-Sent Events обеспечивают коммуникацию в каком направлении?
Практика
В чём ключевое преимущество EventSource перед ручной потоковой передачей через fetch?
В чём ключевое преимущество EventSource перед ручной потоковой передачей через fetch?
Практика
Какие утверждения о формате передачи SSE верны?
Какие утверждения о формате передачи SSE верны?
Was this page helpful?