⏱ 8 minutes 🗓 February 12, 2023

Server Sent Events для фронтенд разработчиков

В данной статье я не буду углубляться в терминологию и архитектуру. Эта статья просто рассказывает рецепты использование SSE на клиенте.

TLDR

  • Не стоит использовать никакие полифилы EventSource из этой статьи и в целом для SSE. Только если вдруг вам нужно поддерживать IE.
  • Авторизацию можно сделать на куках с параметром withCredentials при создании соединения.
  • Используем встроенный в браузер EventSource. Если же нужно больше контроля или функционала (отслеживание событий, парсинг, хедеры) - можно написать свою реализацию поверх fetch streaming, или же просто использовать @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.

Поддержка браузерами

Везде, кроме IE.

caniuse screenshot

Как пользоваться?

Я создал небольшой репозиторий, в котором есть различные примеры использования SSE на сервере и на клиенте.

GitHub - vara855/guide-sse-blg

Page preview image

Contribute to vara855/guide-sse-blg development by creating an account on GitHub.

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

Page preview image

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.

npm

Решаем проблему 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 соединения.