Next.js 15 -fullstack-kehitys (App Router + Server Actions) · Oppitunti

Palvelimen lähettämät tapahtumat reittikäsittelijöistä

Työntäkää reaaliaikaiset päivitykset asiakkaille SSE-suoratoistolla ja uudelleenyhdistämislogiikalla Route Handlerissa.

Oppitunti 1/413 vaihetta

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 muodossa Last-Event-ID
  • retry: <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, jonka Content-Type on text/event-stream
  • Asettakaa arvot Cache-Control: no-cache ja Connection: 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 yhteyden
  • source.onmessage — vastaanottaa nimeämättömiä (vain data:) tapahtumia
  • source.addEventListener('name', fn) — vastaanottaa nimettyjä tapahtumia
  • source.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, jotta WritableStream 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-ID ja toista väliin jääneet tapahtumat
  • Jos haluat estää uudelleenyhdistämisen (esimerkiksi istunnon vanhennuttua), sulje yhteys HTTP-vastauksella 204 No Content — EventSource ei 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 -N poistaa puskuroinnin käytöstä, joten kehykset näkyvät niiden saapuessa
  • -H 'Accept: text/event-stream' jäljittelee sitä, mitä EventSource lähettää
  • Tarkkailkaa tapahtumien välistä tyhjää riviä — jos tyhjä rivi puuttuu, selain ei jäsennä tapahtumia
  • Katkaiskaa yhteys painamalla Ctrl-C ja 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ät data:, event:, id: ja retry:, ja jotka on erotettu tyhjillä riveillä
  • Route Handler: palauttakaa ReadableStream, jonka otsakkeina ovat Content-Type: text/event-stream ja Cache-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-ID uudelleenyhdistämisen yhteydessä ja toistakaa väliin jääneet tapahtumat rengaspuskurista
  • Asiakas: käyttäkää EventSource-oliota Client Component -komponentin useEffect-kutsussa; kutsukaa source.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ä.

Aloita maksutta

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

  1. Palvelimen lähettämät tapahtumat reittikäsittelijöistä
  2. WebSocket-palveluiden integrointi serverless-ympäristössä
  3. Tekoälyvastausten suoratoisto token kerrallaan
  4. Läsnäolo, kohdistimet ja reaaliaikainen yhteistyötila
← Takaisin: Next.js 15 -fullstack-kehitys (App Router + Server Actions)