Palvelimen lähettämät tapahtumat reittikäsittelijöistä
Työntäkää reaaliaikaiset päivitykset asiakkaille SSE-suoratoistolla ja uudelleenyhdistämislogiikalla Route Handlerissa.
Palvelimen lähettämät tapahtumat reittikäsittelijöistä on ilmainen Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunti CoddyKitissä. Tämä on oppitunti 1/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. Next.js 15 -fullstack-kehitys (App Router + Server Actions)-kurssilla on yhteensä 4 oppituntia.
Mitä ovat Server-Sent Events?
Server-Sent Events (SSE) on selaimen natiivisti tukema protokolla, jonka avulla palvelin voi lähettää dataa asiakkaalle yhden pitkäkestoisen HTTP-yhteyden kautta. Toisin kuin WebSocketit, SSE on yksisuuntainen — palvelin kirjoittaa ja asiakas lukee.
- Perustuu tavalliseen HTTP/1.1- tai HTTP/2-protokollaan
- Selaimen
EventSource-API huolehtii yhteydestä ja automaattisesta uudelleenyhdistämisestä - Jokainen viesti on UTF-8-tekstikehys, jolla on määritelty siirtoformaatti
- Toimii tavallisten palomuurien ja välityspalvelinten läpi, vaikka ne estäisivät WebSocketit
SSE sopii erinomaisesti hallintapaneeleihin, reaaliaikaisiin syötteisiin, edistymispalkkeihin ja kaikkiin tilanteisiin, joissa vain palvelimen tarvitsee aloittaa päivitykset.
SSE:n siirtoformaatti
SSE-protokolla käyttää text/event-stream-MIME-tyyppiä. Jokainen tapahtuma on tyhjää riviä seuraava lohko tavallisia tekstirivejä:
data: <payload>— varsinainen viestin sisältö (pakollinen)event: <name>— valinnainen mukautettu tapahtumatyyppi (asiakas kuuntelee sitäaddEventListener-metodilla)id: <value>— valinnainen kohdistin, jonka selain lähettää uudelleenyhdistämisen yhteydessä takaisin muodossaLast-Event-IDretry: <ms>— kertoo selaimelle, kuinka kauan sen tulee odottaa ennen uudelleenyhdistämistä
Yksinkertainen tapahtuma näyttää tältä:
// Raw SSE frame sent over the wire (TypeScript string)
const frame =
'id: 42\n' +
'event: stock-update\n' +
'data: {"symbol":"AAPL","price":189.50}\n' +
'retry: 3000\n' +
'\n'; // <-- blank line terminates the event
console.log(frame);SSE:n Route Handlerin luominen
Next.js 15:ssä (App Router) SSE-päätepiste luodaan tavallisena Route Handlerina app/api/-hakemiston sisälle. Tärkeimmät vaatimukset ovat:
- Palauttakaa
Response, jonkaContent-Typeontext/event-stream - Asettakaa arvot
Cache-Control: no-cachejaConnection: keep-alive, jotta välityspalvelimet eivät puskuroi virtaa - Välittäkää vastauksen rungoksi
ReadableStream, jotta Node.js pitää yhteyden avoinna
ReadableStream-konstruktori hyväksyy start-takaisinkutsun, joka vastaanottaa controller-olion — kutsukaa controller.enqueue()-metodia osien lähettämiseen ja controller.close()-metodia yhteyden päättämiseen.
// app/api/sse/route.ts
import { NextRequest } from 'next/server';
export const dynamic = 'force-dynamic'; // never cache this route
export function GET(_req: NextRequest): Response {
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
// Send one event immediately
controller.enqueue(
encoder.encode('data: {"message":"connected"}\n\n')
);
// Close after the first message (demo only)
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
}Jaksottaisten tapahtumien lähettäminen setIntervalilla
Useimmat todelliset SSE-päätepisteet lähettävät tapahtumia aikataulun mukaan tai datan muuttuessa. Käyttäkää setInterval-metodia start-takaisinkutsun sisällä ajastettujen tapahtumien lähettämiseen ja vapauttakaa resurssit cancel-hookissa, kun asiakas katkaisee yhteyden.
Tyhjentäkää ajastin aina cancel-hookin sisällä — muuten ajastin jatkaa suorittamista ja aiheuttaa muistivuodon, vaikka selain sulkisi välilehden.
// app/api/ticker/route.ts
import { NextRequest } from 'next/server';
export const dynamic = 'force-dynamic';
export function GET(_req: NextRequest): Response {
const encoder = new TextEncoder();
let intervalId: ReturnType<typeof setInterval>;
let counter = 0;
const stream = new ReadableStream({
start(controller) {
intervalId = setInterval(() => {
const payload = JSON.stringify({ tick: ++counter, ts: Date.now() });
controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
}, 1000);
},
cancel() {
// Called when the client closes the connection
clearInterval(intervalId);
console.log('SSE client disconnected — interval cleared');
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
}Nimettyjen tapahtumien ja tunnisteiden lähettäminen
Nimettyjen tapahtumien ja tapahtumatunnisteiden käyttö antaa asiakkaalle tarkemman hallinnan. Nimettyjen tapahtumien avulla käyttöliittymän eri osat voivat tilata tiettyjä tapahtumatyyppejä, kun taas tunnisteet mahdollistavat jatkettavat virrat — selain lähettää viimeksi vastaanotetun tunnisteen Last-Event-ID-otsakkeena uudelleenyhdistämisen yhteydessä, jotta palvelin voi toistaa väliin jääneet tapahtumat.
- Kehysmuoto:
id: N\nevent: name\ndata: ...\n\n - Asiakas kuuntelee
source.addEventListener('name', handler)-kutsulla - Lukekaa
request.headers.get('last-event-id')Route Handlerissa jatkamista varten
// app/api/events/route.ts
import { NextRequest } from 'next/server';
export const dynamic = 'force-dynamic';
function encodeEvent(id: number, event: string, data: unknown): Uint8Array {
const encoder = new TextEncoder();
const frame =
`id: ${id}\n` +
`event: ${event}\n` +
`data: ${JSON.stringify(data)}\n\n`;
return encoder.encode(frame);
}
export function GET(req: NextRequest): Response {
const lastId = Number(req.headers.get('last-event-id') ?? '0');
let id = lastId;
let intervalId: ReturnType<typeof setInterval>;
const stream = new ReadableStream({
start(controller) {
intervalId = setInterval(() => {
id++;
controller.enqueue(
encodeEvent(id, 'notification', { message: `Event ${id}` })
);
}, 2000);
},
cancel() {
clearInterval(intervalId);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
}SSE:n kuluttaminen asiakkaan EventSource-ohjelmointirajapinnalla
Selaimen sisäänrakennettu EventSource-API muodostaa yhteyden SSE-päätepisteeseen ja yhdistää automaattisesti uudelleen, jos yhteys katkeaa. Käyttäkää sitä Client Componentissa useEffect-hookin avulla tilataksenne tapahtumia komponentin liitettäessä ja peruuttaaksenne tilauksen sen irrotessa.
new EventSource('/api/sse')— avaa yhteydensource.onmessage— vastaanottaa nimeämättömiä (vaindata:) tapahtumiasource.addEventListener('name', fn)— vastaanottaa nimettyjä tapahtumiasource.close()— sulkee yhteyden ja lopettaa uudelleenyhdistämisyritykset
'use client';
import { useEffect, useState } from 'react';
type Tick = { tick: number; ts: number };
export default function LiveTicker() {
const [latest, setLatest] = useState<Tick | null>(null);
useEffect(() => {
const source = new EventSource('/api/ticker');
source.onmessage = (e: MessageEvent<string>) => {
setLatest(JSON.parse(e.data) as Tick);
};
source.onerror = () => {
// EventSource will reconnect automatically after 3 s (default)
console.warn('SSE error — browser will retry');
};
return () => source.close(); // cleanup on unmount
}, []);
if (!latest) return <p>Waiting for first tick…</p>;
return (
<p>
Tick <strong>{latest.tick}</strong> received at{' '}
{new Date(latest.ts).toLocaleTimeString()}
</p>
);
}Last-Event-ID:n lukeminen jatkettavia virtoja varten
Kun EventSource yhdistää uudelleen, se liittää automaattisesti mukaan Last-Event-ID-otsakkeen. Palvelin voi lukea tämän arvon ja toistaa kaikki asiakkaalta väliin jääneet tapahtumat — näin virtaa voidaan jatkaa ilman ylimääräistä asiakaskoodia.
Tyypillisessä ratkaisussa viimeisimmät tapahtumat tallennetaan lyhyeen muistissa olevaan rengaspuskuriin (tai tuotannossa Redis-listaan) tunnisteen perusteella, minkä jälkeen kaikki tapahtumat, joiden id > lastId, toistetaan ennen reaaliaikaisen lähetyksen jatkamista.
// Simplified in-memory event log (single-instance demo)
const recentEvents: Array<{ id: number; data: string }> = [];
let globalId = 0;
export function recordEvent(data: string) {
globalId++;
recentEvents.push({ id: globalId, data });
if (recentEvents.length > 100) recentEvents.shift(); // ring buffer
}
export function getEventsSince(lastId: number) {
return recentEvents.filter((e) => e.id > lastId);
}
// In the Route Handler:
// const missed = getEventsSince(Number(req.headers.get('last-event-id') ?? '0'));
// for (const e of missed) controller.enqueue(encodeEvent(e.id, 'update', e.data));AbortSignalin käsittely hallittua sulkemista varten
Next.js 15 tarjoaa request.signal-ominaisuuden (AbortSignal), joka laukeaa, kun asiakas siirtyy pois sivulta tai sulkee välilehden. Sen kuunteleminen on luotettavampaa kuin ReadableStream cancel()-hookin käyttäminen ympäristöissä, joissa suoritus tapahtuu Node.js:n HTTP/2:ssa tai edge-ajonaikaisessa ympäristössä.
req.signal.addEventListener('abort', cleanup)- Yhdistäkää tämä
cancel-hookiin varmistuskeinona resurssien vapauttamiseksi - Tarkistakaa aina
controller.enqueue-kutsun yhteydessä, ettei abort ole tapahtunut, jottaWritableStream closed-virheet vältetään
// app/api/live/route.ts
import { NextRequest } from 'next/server';
export const dynamic = 'force-dynamic';
export function GET(req: NextRequest): Response {
const encoder = new TextEncoder();
let closed = false;
let intervalId: ReturnType<typeof setInterval>;
const stream = new ReadableStream({
start(controller) {
req.signal.addEventListener('abort', () => {
closed = true;
clearInterval(intervalId);
controller.close();
});
intervalId = setInterval(() => {
if (closed) return;
const data = JSON.stringify({ time: new Date().toISOString() });
controller.enqueue(encoder.encode(`data: ${data}\n\n`));
}, 1000);
},
cancel() {
closed = true;
clearInterval(intervalId);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
},
});
}Lähettäminen useille asiakkaille
Yksi Route Handler -instanssi käsittelee yhtä asiakasta. Jotta yksi tapahtuma voidaan hajauttaa kaikille yhteydessä oleville asiakkaille, tarvitaan jaettu publish-subscribe-kanava.
- Saman prosessin sisällä:
EventEmitter-singleton toimii yhden instanssin käyttöönotossa (esimerkiksi yksittäisessä Vercel-säilössä tai itse ylläpidetyssä Node.js-palvelimessa) - Usean instanssin ympäristössä: käyttäkää Redis Pub/Subia, Upstashia tai viestijonoa, jotta kaikki replika-instanssit vastaanottavat tapahtuman
Jokainen SSE Route Handler tilaa jaetun emitterin yhteyden muodostamisen yhteydessä ja peruu tilauksen yhteyden katketessa muistivuotojen välttämiseksi.
// lib/sse-bus.ts — singleton EventEmitter (single-instance only)
import { EventEmitter } from 'events';
const bus = new EventEmitter();
bus.setMaxListeners(500); // raise limit for many concurrent clients
export default bus;
// --- app/api/updates/route.ts ---
// import bus from '@/lib/sse-bus';
// import { NextRequest } from 'next/server';
//
// export function GET(req: NextRequest): Response {
// const encoder = new TextEncoder();
// let closed = false;
//
// const stream = new ReadableStream({
// start(controller) {
// const handler = (payload: unknown) => {
// if (closed) return;
// controller.enqueue(
// encoder.encode(`data: ${JSON.stringify(payload)}\n\n`)
// );
// };
// bus.on('update', handler);
// req.signal.addEventListener('abort', () => {
// closed = true;
// bus.off('update', handler);
// controller.close();
// });
// },
// });
//
// return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' } });
// }Uudelleenyhdistämislogiikka ja uudelleenyrityksen vihjeet
Selain yrittää muodostaa katkenneen SSE-yhteyden uudelleen automaattisesti, mutta toimintaa voi säätää:
- Lähetä yhteyden alussa
retry: 5000\n\n(ms), jolloin selain odottaa 5 sekuntia ennen uudelleenyhdistämistä - Lue uudelleenyhdistämisen yhteydessä
Last-Event-IDja toista väliin jääneet tapahtumat - Jos haluat estää uudelleenyhdistämisen (esimerkiksi istunnon vanhennuttua), sulje yhteys HTTP-vastauksella 204 No Content —
EventSourceei yritä uudelleen 204-vastauksen jälkeen
Välitä tunnistetiedot kyselyparametrina tai evästeenä — EventSource ei tue mukautettuja pyyntöotsakkeita.
// Helper that formats a full SSE preamble with retry hint
function sseHeaders(): HeadersInit {
return {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
};
}
function retryFrame(ms: number): string {
return `retry: ${ms}\n\n`;
}
// Unauthorized? Close with 204 to suppress browser retry loop
function unauthorizedSSE(): Response {
return new Response(null, { status: 204 });
}
// Usage in route:
// const token = req.nextUrl.searchParams.get('token');
// if (!isValid(token)) return unauthorizedSSE();
// controller.enqueue(encoder.encode(retryFrame(5000)));SSE-päätepisteiden testaaminen curl-komennolla
Ennen kuin yhdistätte asiakaskomponentin, varmistakaa SSE-virta suoraan päätelaitteessa curl-komennolla. Näin voitte tarkistaa otsakkeet, kehysten muodon ja tapahtumien ajoituksen ilman selainta.
curl -Npoistaa puskuroinnin käytöstä, joten kehykset näkyvät niiden saapuessa-H 'Accept: text/event-stream'jäljittelee sitä, mitäEventSourcelähettää- Tarkkailkaa tapahtumien välistä tyhjää riviä — jos tyhjä rivi puuttuu, selain ei jäsennä tapahtumia
- Katkaiskaa yhteys painamalla
Ctrl-Cja varmistakaa, että palvelin kirjaa peruutuksen / keskeytyksen
// Run your Next.js dev server, then in a second terminal:
// curl -N -H 'Accept: text/event-stream' http://localhost:3000/api/ticker
//
// Expected output (one block per second):
// data: {"tick":1,"ts":1718000001000}
//
// data: {"tick":2,"ts":1718000002000}
//
// Replay from a specific event ID:
// curl -N -H 'Last-Event-ID: 10' http://localhost:3000/api/events
// TypeScript utility — build SSE test frames in unit tests
function parseSSEFrame(raw: string): Record<string, string> {
const result: Record<string, string> = {};
for (const line of raw.split('\n')) {
const colon = line.indexOf(':');
if (colon === -1) continue;
const key = line.slice(0, colon).trim();
const value = line.slice(colon + 1).trim();
result[key] = value;
}
return result;
}
console.log(parseSSEFrame('id: 5\nevent: tick\ndata: {"n":5}\n'));Tietotesti: SSE:n uudelleenyhdistämisen estäminen
Tarkastellaan seuraavaa tilannetta: käyttäjän istuntotunnus on vanhentunut, ja uusi SSE-yhteysyritys saapuu Route Handler -käsittelijäänne. Haluatte, että selain lopettaa uudelleenyritykset automaattisesti.
Mikä HTTP-vastaus palvelimen tulisi palauttaa, jotta EventSource-olion automaattinen uudelleenyhdistämissilmukka estyy?
Oppitunnin kertaus: SSE Route Handler -käsittelijöistä
Tässä oppitunnissa rakensitte täydellisen Server-Sent Events -putken Next.js 15:llä:
- Johtomuoto:
text/event-stream-kehykset, joissa on kentätdata:,event:,id:jaretry:, ja jotka on erotettu tyhjillä riveillä - Route Handler: palauttakaa
ReadableStream, jonka otsakkeina ovatContent-Type: text/event-streamjaCache-Control: no-cache; viekäädynamic = 'force-dynamic' - Siivous: käyttäkää sekä
ReadableStream cancel()-metodia ettäreq.signal-keskeytyskuuntelijaa aikavälien tyhjentämiseen ja muistivuotojen välttämiseen - Jatkaminen: määrittäkää kasvavat tunnukset, lukekaa
Last-Event-IDuudelleenyhdistämisen yhteydessä ja toistakaa väliin jääneet tapahtumat rengaspuskurista - Asiakas: käyttäkää
EventSource-oliota Client Component -komponentinuseEffect-kutsussa; kutsukaasource.close()komponentin poistuessa - Jakaminen: jakakaa
EventEmitter-singleton (yksittäinen instanssi) tai Redis Pub/Sub (useita instansseja) Route Handler -kutsujen välillä - Todennus: välittäkää tunnukset kyselyparametreina tai evästeinä; palauttakaa
204, jotta uudelleenyhdistämissilmukka pysähtyy vanhentuneiden istuntojen yhteydessä
SSE on kevyt, HTTP-natiivi vaihtoehto WebSocketeille palvelimelta asiakkaalle suuntautuvaan suoratoistoon — se sopii erinomaisesti reaaliaikaisiin koontinäyttöihin, ilmoitusvirtoihin ja tekoälyn vastausten suoratoistoon Next.js:ssä.
Opi TypeScript tekoälytuutorin avulla — ilmaiseksi
Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.
- Kurssit
- 22
- Oppitunnit
- 88
Usein kysytyt kysymykset
Onko oppitunti ”Palvelimen lähettämät tapahtumat reittikäsittelijöistä” ilmainen?
Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolun 3 oppituntia, myös oppitunnin “Palvelimen lähettämät tapahtumat reittikäsittelijöistä”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Next.js 15 -fullstack-kehitys (App Router + Server Actions)-kurssilla on yhteensä 4 oppituntia.
Mitä opin oppitunnilla ”Palvelimen lähettämät tapahtumat reittikäsittelijöistä”?
Työntäkää reaaliaikaiset päivitykset asiakkaille SSE-suoratoistolla ja uudelleenyhdistämislogiikalla Route Handlerissa. Harjoittelet Next.js 15 -fullstack-kehitys (App Router + Server Actions)-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.
Tarvitsenko kokemusta aloittaakseni Next.js 15 -fullstack-kehitys (App Router + Server Actions)-opiskelun?
Aiempi kokemus ei ole tarpeen. CoddyKitin Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 1/4.
Kuinka kauan ”Palvelimen lähettämät tapahtumat reittikäsittelijöistä”-oppitunnin suorittaminen kestää?
Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.
Voinko kirjoittaa ja suorittaa koodia tällä Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunnilla?
Kyllä. Jokainen Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.
Kaikki tämän kurssin oppitunnit
- Palvelimen lähettämät tapahtumat reittikäsittelijöistä
- WebSocket-palveluiden integrointi serverless-ympäristössä
- Tekoälyvastausten suoratoisto token kerrallaan
- Läsnäolo, kohdistimet ja reaaliaikainen yhteistyötila