🌐 Detecting your location…

So implementieren Sie vom Server gesendete Ereignisse in Node.js und reagieren im Jahr 2026: Vollständiger Leitfaden

⏱️6 min read  ·  1,285 words

Vom Server gesendete Ereignisse sind die einfachste Möglichkeit, Daten vom Server an den Browser zu übertragen. Sie laufen über normales HTTP, stellen die Verbindung automatisch wieder her und benötigen kein zusätzliches Protokoll. Für Dashboards, Benachrichtigungen, Fortschrittsaktualisierungen und das Streamen von KI-Antworten – alles, wo Daten in eine Richtung fließen – ist SSE normalerweise die richtige Wahl gegenüber WebSockets.

SSE oder WebSockets?

SSE WebSockets
Richtung Nur vom Server zum Client Bidirektional
Protokoll Einfaches HTTP Aktualisieren Sie auf ws://
Automatisch wieder verbinden Eingebaut Sie implementieren es
Datenformat UTF-8-Text Text oder Binär
Proxy-Freundlichkeit Normalerweise in Ordnung Benötigt Upgrade-Unterstützung
Komplexität Niedrig Höher

Verwenden SieSSE für Live-Dashboards, Benachrichtigungs-Feeds, Auftragsfortschritt, Log-Tailing und Token-für-Token-KI-Ausgabe. Verwenden SieWebSockets wenn der Client auch häufig Nachrichten sendet – Chat, gemeinsame Bearbeitung, Multiplayer-Spiele.

Ein üblicher und sinnvoller Hybrid: SSE für Server-Updates, gewöhnliche HTTP-POST-Anfragen für Client-Aktionen. Damit sind die meisten Anwendungen ohne zweites Protokoll abgedeckt.

Das Wire-Format

SSE ist ein reiner Textstream mit einem kleinen, strengen Format.

data: hello world

event: userUpdate
data: {"id":1,"name":"Ada"}

id: 42
retry: 5000
data: message with an id and a retry hint

Zwei Regeln verursachen fast jeden Fehler. Jede Nachricht endet mitzwei Zeilenumbrüche. Und mehrzeilige Daten benötigen eindata: Präfix in jeder Zeile, weshalb JSON-Nutzlasten keine rohen Zeilenumbrüche enthalten dürfen.

Server: Node.js mit Express

import express from 'express';

const app = express();

// Track connected clients so we can broadcast.
const clients = new Set();

app.get('/api/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache, no-transform',
    'Connection': 'keep-alive',
    // Tell nginx not to buffer this response.
    'X-Accel-Buffering': 'no',
  });

  // Flush headers immediately so the client's connection opens.
  res.flushHeaders();

  const client = { id: Date.now(), res };
  clients.add(client);

  send(res, { type: 'connected', at: new Date().toISOString() });

  // A comment line every 25s keeps proxies from closing an idle connection.
  const heartbeat = setInterval(() => {
    res.write(': heartbeat\n\n');
  }, 25_000);

  req.on('close', () => {
    clearInterval(heartbeat);
    clients.delete(client);
  });
});

function send(res, data, event) {
  if (event) res.write(`event: ${event}\n`);
  // JSON.stringify never emits a raw newline, which keeps the frame valid.
  res.write(`data: ${JSON.stringify(data)}\n\n`);
}

export function broadcast(data, event) {
  for (const client of clients) {
    send(client.res, data, event);
  }
}

app.listen(3000);

Drei Details sind hier wichtig. flushHeaders() Öffnet den Stream sofort, anstatt auf den ersten Schreibvorgang zu warten. Der Heartbeat-Kommentar verhindert, dass Vermittler eine inaktive Verbindung trennen. UndX-Accel-Buffering: no verhindert, dass Nginx die Antwort puffert, was der häufigste Grund dafür ist, dass SSE lokal funktioniert und in der Produktion fehlschlägt.

Kunde: Reagieren

import { useEffect, useRef, useState } from 'react';

export function useEventStream(url) {
  const [messages, setMessages] = useState([]);
  const [status, setStatus] = useState('connecting');
  const sourceRef = useRef(null);

  useEffect(() => {
    const source = new EventSource(url, { withCredentials: true });
    sourceRef.current = source;

    source.onopen = () => setStatus('open');

    source.onmessage = (e) => {
      const data = JSON.parse(e.data);
      setMessages(prev => [...prev, data]);
    };

    // Named events need their own listener.
    source.addEventListener('userUpdate', (e) => {
      const data = JSON.parse(e.data);
      setMessages(prev => [...prev, { ...data, kind: 'userUpdate' }]);
    });

    source.onerror = () => {
      // EventSource reconnects on its own unless the state is CLOSED.
      setStatus(source.readyState === EventSource.CLOSED ? 'closed' : 'reconnecting');
    };

    return () => source.close();
  }, [url]);

  return { messages, status };
}
export function LiveFeed() {
  const { messages, status } = useEventStream('/api/events');

  return (
    <div>
      <p>Status: {status}</p>
      <ul>
        {messages.map((m, i) => <li key={i}>{JSON.stringify(m)}</li>)}
      </ul>
    </div>
  );
}

Rückkehrsource.close() aus dem Effekt ist nicht optional. Ohne sie hinterlässt die doppelte Bereitstellung des React Strict Mode eine verwaiste Verbindung, und beim Navigieren in der App sammeln sich offene Streams an, bis das Verbindungslimit pro Domäne des Browsers erreicht ist und alles ins Stocken gerät.

Wiederaufnahme nach einer Verbindungsunterbrechung

Wenn Sie einid:senden Feld, der Browser speichert es und sendet es alsLast-Event-IDzurück bei Wiederverbindung. Dadurch können Sie nur das wiedergeben, was Sie verpasst haben.

app.get('/api/events', (req, res) => {
  // ... headers as above ...

  const lastId = req.headers['last-event-id'];
  if (lastId) {
    for (const event of getEventsSince(Number(lastId))) {
      res.write(`id: ${event.id}\n`);
      res.write(`data: ${JSON.stringify(event.payload)}\n\n`);
    }
  }
});

Dadurch wird SSE von Best-Effort zu etwas, das einer zuverlässigen Zustellung näher kommt, was für Benachrichtigungs-Feeds wichtig ist, bei denen eine abgelegte Nachricht für den Benutzer sichtbar ist.

Authentifizierung

Der EingeboreneEventSource Die API kann keine benutzerdefinierten Header festlegen, was Leute überrascht, die tokenauthentifizierte APIs erstellen. Drei praktikable Ansätze:

Kekse – am einfachsten. PasswithCredentials: true und lassen Sie das Sitzungscookie die Anfrage wie jede andere authentifizieren.

Ein kurzlebiges Token in der Abfragezeichenfolge – Nur akzeptabel, wenn das Token einmalig ist und in wenigen Minuten abläuft, da URLs in Serverprotokollen landen.

mit einem Streaming-Reader abrufen – vollständige Header-Kontrolle, auf Kosten der Implementierung der Wiederverbindung selbst.

async function streamWithAuth(url, token, onMessage) {
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${token}` },
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const frames = buffer.split('\n\n');
    buffer = frames.pop() ?? '';       // keep the incomplete frame

    for (const frame of frames) {
      const line = frame.split('\n').find(l => l.startsWith('data:'));
      if (line) onMessage(JSON.parse(line.slice(5).trim()));
    }
  }
}

Beachten Sie die Pufferung: Ein Netzwerkblock kann einen Frame in zwei Hälften teilen, daher müssen Sie den Rest behalten und nur vollständige Frames analysieren. Das unabhängige Parsen jedes Blocks führt zu zeitweiligen JSON-Fehlern, deren Diagnose schwierig ist.

Streaming von KI-Antworten

Das Muster hinter der Token-für-Token-Ausgabe in Chat-Schnittstellen.

app.post('/api/chat', async (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache, no-transform',
    'X-Accel-Buffering': 'no',
  });
  res.flushHeaders();

  try {
    for await (const chunk of generateResponse(req.body.prompt)) {
      res.write(`data: ${JSON.stringify({ token: chunk })}\n\n`);
    }
    res.write('data: [DONE]\n\n');
  } catch (err) {
    res.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`);
  } finally {
    res.end();
  }
});

Proxy- und Bereitstellungskonfiguration

Bei den meisten SSE-Fehlern in der Produktion handelt es sich um Proxy-Pufferung. Der Header allein reicht nicht immer aus.

# nginx
location /api/events {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection '';
    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 24h;
    chunked_transfer_encoding off;
}

Beachten Sie auch, dass serverlose Plattformen häufig die Reaktionszeit begrenzen, was langlebige SSE-Verbindungen ungeeignet macht. Überprüfen Sie die Grenzen Ihrer Plattform, bevor Sie darauf aufbauend entwerfen. Und mit HTTP/1.1 erlauben Browser nur etwa sechs Verbindungen pro Domäne – HTTP/2 beseitigt diese Einschränkung, also stellen Sie SSE über HTTP/2 bereit, wo immer Sie können.

Häufige Fehler

Den zweiten Zeilenumbruch vergessen. Die Nachricht wird nie versendet und der Client scheint zu hängen.

Die EventSource wird beim Unmounten nicht geschlossen. Die Verbindungen sammeln sich an, bis das Browser-Limit erreicht ist.

Rohe Zeilenumbrüche in Daten. ImmerJSON.stringify die Nutzlast.

Kein Herzschlag. Ungenutzte Verbindungen werden von Vermittlern nach ein oder zwei Minuten geschlossen.

Proxy-Pufferung bleibt aktiviert. Alles funktioniert lokal, dann kommt nichts in der Produktion an, bis die Antwort endet.

Verwendung von SSE für bidirektionalen Datenverkehr. Wenn der Client häufig Nachrichten sendet, verwenden Sie WebSockets.

Fazit

SSE ermöglicht Ihnen Server-zu-Client-Streaming über normales HTTP mit automatischer Wiederverbindung und sehr wenig Code. Machen Sie fünf Dinge richtig:Beenden Sie jede Nachricht mit zwei Zeilenumbrüchen, senden Sie einen regelmäßigen Heartbeat-Kommentar, deaktivieren Sie die Proxy-Pufferung sowohl im Header als auch in der Nginx-Konfiguration, schließen Sie die EventSource in Ihrer Effektbereinigung und verwenden Sieid: mitLast-Event-ID wenn verpasste Nachrichten bemerkt würden. Greifen Sie nur dann zu WebSockets, wenn der Client wirklich häufig Nachrichten zurücksenden muss.

MD Rafikul Islam

Written by

MD Rafikul Islam is a software developer and the editor of TechPulse. He writes about developer tooling, hardware, and the practical decisions that come up in day-to-day engineering work — which laptop to buy, which framework to commit to, why a build broke at 2am. He tests the tools he writes about and says plainly when something is not worth the money. Corrections and corrections requests are welcome at rony.yf25@gmail.com.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা