Server-Sent Events в TypeScript: пошаговое руководство для реальных проектов

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Server-Sent Events в TypeScript: стриминг данных без головной боли

Представьте: ваш сервер генерирует данные — логи, уведомления, обновления курса валют — и должен мгновенно отправлять их на фронтенд. WebSocket? Мощно, но избыточно. Polling? Тормозит и жрёт трафик. Есть третий путь — элегантный, простой и невероятно полезный: Server-Sent Events (SSE).

В этой статье мы разберём SSE на TypeScript от и до. Напишем клиент и сервер, научимся обрабатывать ошибки и переподключаться, а главное — поймём, когда SSE действительно выигрывает перед другими подходами. Поехали!

Что такое Server-Sent Events и зачем они нужны?

Server-Sent Events — это стандарт HTML5, который позволяет серверу отправлять данные клиенту по одному HTTP-соединению в реальном времени. В отличие от WebSocket, SSE односторонний: только сервер шлёт сообщения. Но именно в этом его сила — он максимально прост и не требует дополнительных библиотек.

Когда выбирать SSE?

  • Односторонний стриминг: уведомления, логи, курсы валют, тикеры акций.
  • Простая интеграция: работает через стандартный EventSource в браузере — никаких npm-пакетов.
  • Автоматическое переподключение: браузер сам восстанавливает соединение при обрыве.
  • Совместимость с HTTP/2: мультиплексирование не мешает, в отличие от WebSocket.

Но помните: SSE не умеет отправлять данные от клиента серверу (для этого используйте обычные POST-запросы) и не поддерживается в старых браузерах (IE — привет).

Серверная часть: пишем SSE на TypeScript + Express

Начнём с бэкенда. Создадим простой сервер на Node.js с Express и TypeScript. Установите зависимости:

npm init -y
npm install express cors
npm install -D typescript @types/express @types/node ts-node
npx tsc --init

Теперь напишем сам сервер. Ключевой момент — установка правильных заголовков и формат сообщений.

// server.ts
import express, { Request, Response } from 'express';
import cors from 'cors';

const app = express();
app.use(cors());

// Эндпоинт для SSE
app.get('/events', (req: Request, res: Response) => {
  // Устанавливаем заголовки для SSE
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
  });

  // Отправляем начальное сообщение
  res.write('data: {"message": "Соединение установлено"}\n\n');

  // Отправляем событие каждые 3 секунды
  const intervalId = setInterval(() => {
    const data = JSON.stringify({
      time: new Date().toISOString(),
      value: Math.random() * 100,
    });
    res.write(`data: ${data}\n\n`);
  }, 3000);

  // Обрабатываем отключение клиента
  req.on('close', () => {
    clearInterval(intervalId);
    res.end();
    console.log('Клиент отключился');
  });
});

const PORT = 3000;
app.listen(PORT, () => console.log(`Сервер SSE запущен на порту ${PORT}`));

Обратите внимание на формат: каждое сообщение начинается с data: и заканчивается двумя переводами строки \n\n. Можно также указывать id, event и retry — но это база.

Отправляем именованные события

Иногда нужно различать типы сообщений. Добавим поле event:

res.write('event: update\n');
res.write(`data: ${JSON.stringify({ id: 1, title: 'Новый пост' })}\n\n`);

// А потом — другое событие
res.write('event: notification\n');
res.write('data: {"level": "info", "text": "Сервер работает"}\n\n');

На клиенте мы сможем подписаться на конкретные события.

Клиентская часть: принимаем события на TypeScript

Теперь напишем клиент. В браузере используется встроенный EventSource. Но если вы работаете в Node.js (например, для тестов), понадобится полифилл — eventsource или node-fetch с SSE.

Вот пример для браузера на TypeScript:

// client.ts
interface ServerEvent {
  time: string;
  value: number;
}

class SSEClient {
  private eventSource: EventSource | null = null;
  private url: string;

  constructor(url: string) {
    this.url = url;
  }

  connect(): void {
    this.eventSource = new EventSource(this.url);

    // Слушаем все сообщения
    this.eventSource.onmessage = (event: MessageEvent) => {
      const data: ServerEvent = JSON.parse(event.data);
      console.log('Получены данные:', data);
      this.handleData(data);
    };

    // Именованные события
    this.eventSource.addEventListener('update', (event: MessageEvent) => {
      const data = JSON.parse(event.data);
      console.log('Обновление:', data);
    });

    this.eventSource.addEventListener('notification', (event: MessageEvent) => {
      const data = JSON.parse(event.data);
      console.log('Уведомление:', data);
    });

    // Обработка ошибок и переподключение
    this.eventSource.onerror = (error) => {
      console.error('Ошибка SSE:', error);
      // EventSource сам переподключается, но можно добавить логику
      if (this.eventSource?.readyState === EventSource.CLOSED) {
        console.log('Соединение закрыто. Попробуем переподключиться через 5 сек...');
        setTimeout(() => this.connect(), 5000);
      }
    };
  }

  private handleData(data: ServerEvent): void {
    // Ваша бизнес-логика
    console.log(`Время: ${data.time}, Значение: ${data.value.toFixed(2)}`);
  }

  disconnect(): void {
    this.eventSource?.close();
    this.eventSource = null;
  }
}

// Использование
const client = new SSEClient('http://localhost:3000/events');
client.connect();

Обратите внимание: EventSource автоматически обрабатывает переподключение, если сервер присылает retry в заголовке или в теле события. Но мы добавили свою логику на случай полного закрытия.

Продвинутые техники: идентификаторы и переподключение

SSE поддерживает поле id. Когда клиент переподключается, он отправляет заголовок Last-Event-ID с последним полученным ID. Сервер может использовать это, чтобы не дублировать данные.

// Сервер: добавляем ID
let eventId = 0;
const intervalId = setInterval(() => {
  eventId++;
  const data = JSON.stringify({ id: eventId, message: `Событие #${eventId}` });
  res.write(`id: ${eventId}\n`);
  res.write(`data: ${data}\n\n`);
}, 2000);

На клиенте EventSource сам запомнит последний ID и отправит его при переподключении. Вам не нужно ничего делать — магия браузера.

Настраиваем retry

Сервер может указать клиенту, через сколько миллисекунд пытаться переподключиться:


                
                

Блоги

Рекомендаций по книгам