Server Sent Events для фронтенд разработчиков
В данной статье я не буду углубляться в терминологию и архитектуру. Эта статья просто рассказывает рецепты использование SSE на клиенте.
TLDR
- Не стоит использовать никакие полифилы
EventSourceиз этой статьи и в целом дляSSE. Только если вдруг вам нужно поддерживать IE. - Авторизацию можно сделать на куках с параметром
withCredentialsпри создании соединения. - Используем встроенный в браузер
EventSource. Если же нужно больше контроля или функционала (отслеживание событий, парсинг, хедеры) - можно написать свою реализацию поверхfetchstreaming, или же просто использовать @microsoft/fetch-event-source реализацию.
Что такое SSE?
Server-sent events (SSE) - это технология, которая позволяет нам получать данные из бекенда по-ивентно (by-event). После установления HTTP соединения, мы можем подписаться на событие получения сообщения из бекенда, а также на событие error. По сути, для пользователя этой технологии - это WebSocket, в который нельзя отправить сообщения серверу.
EventSource - Web интерфейс для использования server-sent events.
Где его использовать?
Для себя я сформулировал так:
Если мне нужна одно-направленная связь с бэкендом -
EventSourceмой выбор. Во всех остальных случаях -WebSocketили простоHTTP. (Ну если прям очень нужно - можно иgrpc…);
Особенности и странности браузерной реализации
- Если соединение закрывается на стороне сервера - то клиент получит событие
error, из которого никак нельзя будет понять, что это нормальное закрытие соединения. - Нельзя ничего передать дополнительно к
URL, кроме как флагаwithCredentials.
Поддержка браузерами

Как пользоваться?
Я создал небольшой репозиторий, в котором есть различные примеры использования SSE на сервере и на клиенте.
GitHub - vara855/guide-sse-blg
Contribute to vara855/guide-sse-blg development by creating an account on GitHub.
git clone git@github.com:vara855/guide-sse-blg.git
Пример реализации SSE route
function createSseEvent(data) {
return `data: ${data}\n\n`;
}
async function onDigits(req, res) {
console.log(`open -> ${req.url}`);
res.writeHead(200, {
"Content-Type": "text/event-stream; charset=utf-8",
"Cache-Control": "no-cache",
});
let i = 0;
for await (const startTime of setInterval(1000, Date.now())) {
const now = Date.now();
i++;
const message = `Event #${i} time: ${new Date().toLocaleTimeString()}`;
res.write(createSseEvent(message));
console.log(`Produced message: "${message}"`);
if (now - startTime > 10000) {
res.write(
"event: finish\ndata: All of the events were sent. Closing connection.\n\n"
);
console.log(`close response ${req.url}`);
res.end();
break;
}
}
}
Как можно заметить, протокол SSE - текстовый. Каждое сообщение - это строка, которая должна содержать данные (data) и должна заканчиваться на \n\n. Спецификацию можно почитать здесь.
Также в сообщении может присутствовать идентификатор id и название события event.
То есть сообщения могут выглядеть как:
# one event
event: name-of-event
data: event-data
# another one
data: event-data-2
id: event-id
Обязательным также является хедер с типом контента text/event-stream.
Обработка на клиенте
Воспользоваться этим эндпоинтом на клиенте максимально просто, нужно только лишь создать экземпляр EventSource и навесить обработчики событий.
const client = new EventSource("/be/digits", { withCredentials: true });
client.on('message', (event) => {
console.log('Data received', event.data);
})
client.on('close', () => { /* ... */ });
client.on('open', () => { /* ... */});
client.addEventListener('eventType', evt => { /* ... */ });
Больше ничего браузерный EventSource не позволяет делать.
Небольшая демка бекенда и фронтенда в этом репозитории. Для запуска следуте выполнить этот скрипт:
npm run vanilla-demo
Проблема EventSource интерфейса
Он не конфигурабельный от слова совсем. Из-за этого многие люди не используют его, и вы можете найти много вопросов на StackOverFlow об этом простом интерфейсе (#1, #2, …).
Вместо встроенного в браузеры интерфейса, многие разработчики приходят к использованию полифилов, которые позволяют модифицировать хедеры, или же менять другие настройки подключения.
Самые популярные полифилы:
Настоятельно рекомендую обходить все эти полифилы стороной.
У последней можно обнаружить “интересную” надпись в ридми.
Они довольно популярны, и это грустно 😟. Потому что, качество кода в них крайне низкое, а также если вы попробуете ими воспользоваться, вы наткнётесь на различия в работе с “нативным” EventSource.
Демонстрацию различий можно увидеть запустив скрипт
npm run vanilla-polyfill-demo
Fetch реализация SSE
@microsoft/fetch-event-source
A better API for making Event Source requests, with all the features of fetch(). Latest version: 2.0.1, last published: 3 years ago. Start using @microsoft/fetch-event-source in your project by running `npm i @microsoft/fetch-event-source`. There are 80 other projects in the npm registry using @microsoft/fetch-event-source.
Решаем проблему Auth хедеров
Первый Способ
Используем аутентификацию по кукам, по которой бэкенд сможет определить, что мы авторизованный пользователь.
Конечно, сейчас чаще всего используется OAuth и скорее всего у вас просто к каждому запросу на бекенд идёт Bearer хэдер с токеном из сессии, так что это не особо подходящий вариант для всех.
Второй способ
Так как SSE работает поверх HTTP в отличии от WebSockets, мы можем реализовать SSE с помощью fetch. Уточню, что не нужно реализовывать EventSource полифил, нужно лишь реализовать своеобразный fetch streaming API.
Если вы попробуете сделать это сами, то вы наткнётесь на некоторые проблемы с обрывками сообщений, вам нужно будет самим буферизировать и склеивать последовательности сообщений. В замен своей реализации стоит посмотреть в сторону этого npm пакета @microsoft/fetch-event-source, ну или вдохновиться им.
👍 Какие плюсы?
- Поддерживается Page Visibility API;
- Кастомизация всего, что можно кастомизировать в
fetch.
👎 Минусы:
- При просмотре таких запросов в DevTools (chrome, firefox и может в других тоже), вы не увидите красивую табличку сообщений, как если бы это был нативный
EventSource. Но это можно решить просто логированием, или любым другим угодным вам способом - Issue #3 - Не нативно - да, мир JS наполнен сотнями реализаций одного и того же, все они работают по разному и хочется хороших, встроенных в Web стандарты интерфейсов, но, некоторые части Web развиваются очень медленно и иногда не меняются годами.
Проблема HTTP метода
EventSource работает по HTTP с методом GET. Как мы знаем, GET в браузере - не позволяет передавать body запроса. И если вам очень нужно использовать EventSource с методом POST и каким-то body - вы можете также использовать @microsoft/fetch-event-source.
Но, моё имхо, что это просто напросто не правильно даже семантически.
Я бы вам предложил всё таки создать отдельным HTTP POST/PUT/... запросом какой либо ресурс (он может быть временным, и храниться только в памяти бэкенда или в каком-то redis…), а затем уже, использовать идентификатор этого ресурса при создании SSE соединения.