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
Сервер может указать клиенту, через сколько миллисекунд пытаться переподключиться:
Блоги
Рекомендаций по книгам
Читайте также
Kotlin roadmap 2025: с нуля до профи — что учить и в каком порядке
Как go test запускает все тесты проекта: разбор под капотом
«Could not find or load main class» в Java: что делать новичку?
Функциональное программирование в C++: продвинутый уровень
htmlentities() vs htmlspecialchars() в PHP: полный разбор с примерами