Vastausten suoratoisto ja ReadableStream-käsittelijöissä
Palauttakaa dataa vähitellen ReadableStreamilla esimerkiksi tekoälytokeneita, lokeja ja vaiheittain muodostuvia hyötykuormia varten.
Vastausten suoratoisto ja ReadableStream-käsittelijöissä on ilmainen Next.js 15 -fullstack-kehitys (App Router + Server Actions)-oppitunti CoddyKitissä. Tämä on oppitunti 3/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.
Miksi vastaus kannattaa suoratoistaa?
Tavallinen Route Handler muodostaa koko rungon muistiin ja lähettää sen sitten kerralla. Tekoälyn token-tulosteen, reaaliaikaisten lokien tai suurten raporttien tapauksessa käyttäjä näkee tyhjän näytön aivan loppuun asti.
Suoratoisto muuttaa tilanteen: lähetätte osia asiakkaalle heti niiden valmistuttua. Selain alkaa näyttää ensimmäisiä tavuja välittömästi, ensimmäisen tavun saapumiseen kuluva aika lyhenee, eikä koko hyötykuormaa tarvitse koskaan pitää RAM-muistissa.
ReadableStreamon Web Standard -perusprimitiivi, jota Next.js 15 käyttää tähän.- Palautatte sen suoraan Route Handlerista
Response-olion sisällä. - Se toimii sekä Node.js- että Edge-ajoympäristöissä.
ReadableStream-rakenne
ReadableStream luodaan oliolla, joka sisältää start(controller)-metodin. Sen sisällä kutsutte controller.enqueue(chunk)-metodia datan lähettämiseen ja controller.close()-metodia, kun kaikki on valmis.
Osan tulisi olla tavuista koostuva Uint8Array. TextEncoder muuntaa merkkijonon näiksi tavuiksi. Kyseessä on puhdas Web API -koodi, joten se toimii kaikkialla, missä on nykyaikainen JS-moottori.
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("Hello, "));
controller.enqueue(encoder.encode("streamed "));
controller.enqueue(encoder.encode("world!"));
controller.close();
},
});
const response = new Response(stream);
console.log("Stream and Response created:", response instanceof Response);Virran palauttaminen käsittelijästä
Tiedostossa app/api/.../route.ts viette HTTP-metodifunktion (tässä GET) ja palautatte Response-olion, jonka runkona virta toimii.
Asettakaa aina Content-Type. Tavalliselle asteittain lähetettävälle tekstille text/plain riittää; rakenteisille tapahtumavirroille käytätte text/event-stream-tyyppiä (käsitellään myöhemmin).
Tämä on kehyskoodia, joka tarvitsee Next.js-palvelimen, joten sitä ei voi suorittaa itsenäisesti.
// app/api/hello/route.ts
export async function GET(): Promise<Response> {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode("chunk-1\n"));
controller.enqueue(encoder.encode("chunk-2\n"));
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}Suoratoisto ajan kuluessa async start -menetelmällä
Suoratoiston todellinen voima näkyy, kun osat saapuvat ajan kuluessa. start-metodi voi olla async, jolloin voitte käyttää await-lauseketta enqueue-kutsujen välillä.
Tässä pieni viive simuloi työtä (tekoälypalveluntarjoajaa, hidasta kyselyä tai työvaihetta). Kukin rivi saapuu asiakkaalle heti, kun se lisätään jonoon, eikä vasta silmukan päätyttyä.
- Käyttäkää
await-lauseketta tulosteen rytmittämiseen estämättä tapahtumasilmukkaa. - Älkää koskaan unohtako
controller.close()-kutsua, muuten yhteys jää avoimeksi.
const encoder = new TextEncoder();
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const stream = new ReadableStream({
async start(controller) {
for (let i = 1; i <= 3; i++) {
await sleep(50);
controller.enqueue(encoder.encode(`step ${i}\n`));
}
controller.close();
},
});
const reader = stream.getReader();
const decoder = new TextDecoder();
let out = "";
let result = await reader.read();
while (!result.done) {
out += decoder.decode(result.value);
result = await reader.read();
}
console.log(out.trim());Tekoälyn tokenien suoratoisto
Klassinen käyttötapaus on LLM:n tokenivirran välittäminen suoraan selaimeen, jolloin teksti ilmestyy sana sanalta. Useimmat AI SDK:t tarjoavat osittaisista osista koostuvan asynkronisen iteroitavan.
Käytte iteroitavan läpi start-metodin sisällä ja lisäätte jonoon kunkin tokenin tekstimuutoksen. Käyttäjä näkee vastauksen muodostuvan reaaliajassa aivan kuten chat-käyttöliittymässä.
// app/api/chat/route.ts
import { openai } from "@/lib/openai";
export async function POST(req: Request): Promise<Response> {
const { prompt } = await req.json();
const encoder = new TextEncoder();
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
stream: true,
messages: [{ role: "user", content: prompt }],
});
const stream = new ReadableStream({
async start(controller) {
for await (const part of completion) {
const token = part.choices[0]?.delta?.content ?? "";
if (token) controller.enqueue(encoder.encode(token));
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}Server-Sent Events (SSE) -muoto
Kun tarvitsette nimettyjä ja rakenteisia tapahtumia, jotka selaimen EventSource ymmärtää, käyttäkää SSE-siirtomuotoa ja text/event-stream-sisältötyyppiä.
Jokainen viesti on rivi, joka alkaa tekstillä data: ja jota seuraa hyötykuorma. Viestin päättää kaksi rivinvaihtoa (\n\n). Voitte sarjallistaa JSONin tekstin data: jälkeen.
Content-Type: text/event-streamCache-Control: no-cache, jotta välityspalvelimet eivät puskuroi sisältöä.Connection: keep-aliveNode-ajoympäristössä.
// app/api/events/route.ts
export async function GET(): Promise<Response> {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (const status of ["queued", "running", "done"]) {
const payload = JSON.stringify({ status });
controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
await new Promise((r) => setTimeout(r, 300));
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}Edistymislokien suoratoisto
Pitkäkestoiset työt (tuonnit, koontiversiot ja eräkäsittely) hyötyvät edistymislokin suoratoistosta. Jokainen valmis vaihe lisätään jonoon, jolloin asiakas voi päivittää reaaliaikaista konsolia ilman kyselyitä.
Toimintamalli on sama: suorittakaa työ, lisätkää rivi jonoon ja toistakaa. Alla on suoritettava simulaatio monivaiheisesta työstä, joka tuottaa NDJSON-muotoa (yhden JSON-olion riville).
const encoder = new TextEncoder();
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const steps = ["fetch", "transform", "upload"];
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < steps.length; i++) {
await sleep(30);
const line = JSON.stringify({ step: steps[i], pct: ((i + 1) / steps.length) * 100 });
controller.enqueue(encoder.encode(line + "\n"));
}
controller.close();
},
});
const reader = stream.getReader();
const decoder = new TextDecoder();
let buf = "";
let r = await reader.read();
while (!r.done) {
buf += decoder.decode(r.value);
r = await reader.read();
}
for (const l of buf.trim().split("\n")) console.log(JSON.parse(l).step);Asiakkaan yhteyden katkeamisen käsittely
Jos käyttäjä sulkee välilehden suoratoiston aikana, työnteko kannattaa lopettaa. Request-olio sisältää req.signal-kohdassa AbortSignal-olion, joka aktivoituu yhteyden katketessa.
Tarkistakaa silmukan sisällä req.signal.aborted ja käyttäkää halutessanne virran cancel()-takaisinkutsua resurssien vapauttamiseen (sulkekaa esimerkiksi LLM-yhteys tai keskeyttäkää tietokantakursori).
// app/api/long/route.ts
export async function GET(req: Request): Promise<Response> {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 100; i++) {
if (req.signal.aborted) break; // client left
controller.enqueue(encoder.encode(`tick ${i}\n`));
await new Promise((r) => setTimeout(r, 200));
}
controller.close();
},
cancel(reason) {
console.log("stream cancelled:", reason);
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}Virheiden käsittely virran sisällä
Kun olette palauttaneet Response-olion, HTTP-tila on jo 200 ja otsakkeet on lähetetty. Ette voi vaihtaa tilaksi 500:tä kesken suoratoiston.
Ympäröikää siis riskialtis työ try/catch-rakenteella ja välittäkää virheet osana virtaa (esimerkiksi SSE:n event: error -rivi tai JSON-virheolio), minkä jälkeen sulkekaa virta. Käyttäkää controller.error(e)-kutsua vain, kun haluatte lopettaa virran äkillisesti.
// app/api/job/route.ts
export async function GET(): Promise<Response> {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
try {
const data = await riskyWork();
controller.enqueue(encoder.encode(JSON.stringify(data) + "\n"));
} catch (err) {
const msg = err instanceof Error ? err.message : "unknown";
controller.enqueue(encoder.encode(JSON.stringify({ error: msg }) + "\n"));
} finally {
controller.close();
}
},
});
return new Response(stream, {
headers: { "Content-Type": "application/x-ndjson" },
});
}Edge-ajoympäristö ja vastapaine
Suoratoisto toimii erinomaisesti Edge-ajoympäristössä. Ottakaa se käyttöön komennolla export const runtime = "edge". Edge-ajoympäristö perustuu Web Streams -rajapintoihin, joten sama ReadableStream-koodi toimii muuttamattomana ja alkaa välittyä välittömästi käyttäjää lähellä sijaitsevasta sijainnista.
Vastapaine: jos asiakas lukee hitaasti, controller.enqueue puskuroi silti sisältöä. Jos tuottaja tuottaa paljon dataa, suosikaa pull-pohjaista lähdettä tai tarkistakaa controller.desiredSize tahdin säätämiseksi ja rajoittamattoman muistin kasvun välttämiseksi.
// app/api/edge-stream/route.ts
export const runtime = "edge";
export async function GET(): Promise<Response> {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (let i = 0; i < 5; i++) {
// desiredSize < 0 means the consumer is behind
if ((controller.desiredSize ?? 1) > 0) {
controller.enqueue(encoder.encode(`edge ${i}\n`));
}
await new Promise((r) => setTimeout(r, 100));
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain; charset=utf-8" },
});
}Virran kuluttaminen asiakkaalla
Selaimen puolella fetch antaa käyttöön response.body-olion, joka on itsessään ReadableStream. Lukekaa sitä lukijalla ja purkakaa osat niiden saapuessa, jotta käyttöliittymä päivittyy vaiheittain.
SSE:tä varten voitte käyttää sen sijaan natiivista EventSource-rajapintaa. Raakatekstille tai NDJSON:lle alla oleva lukusilmukka on yleiskäyttöinen ratkaisu.
// components/StreamReader.ts
export async function readStream(url: string, onChunk: (text: string) => void) {
const res = await fetch(url);
if (!res.body) throw new Error("No response body to stream");
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
onChunk(decoder.decode(value, { stream: true }));
}
}Pikatarkistus
Testatkaa ymmärrystänne suoratoistavista Route Handlers -toiminnoista.
Kertaus
Opitte suoratoistamaan vaiheittain saapuvaa dataa Next.js 15:n Route Handlers -toiminnoista:
- ReadableStream yhdessä
start(controller)-metodin sekäcontroller.enqueue()- jacontroller.close()-kutsujen kanssa on keskeinen perusprimitiivi; palauttakaa seResponse-olion sisällä. - async start mahdollistaa odottamisen osien välillä tekoälyn tokeneita, edistymislokeja ja SSE-tapahtumia varten.
- Käyttäkää SSE:tä varten yhdistelmää
text/event-stream+data: ...\n\ntai rivinvaihtoon perustuvaan JSONiin NDJSON-muotoa. - Tarkkailkaa yhteyden katkeamista varten arvoa
req.signal.abortedja vapauttakaa resurssitcancel()-kutsussa. - Tila ja otsakkeet lukitaan palautuksen yhteydessä, joten raportoi kesken suoratoiston ilmenevät virheet osina virtaa.
- Edge-ajoympäristö (
runtime = "edge") suorittaa samaa Web Streams -koodia; huomioikaa vastapaine arvondesiredSizeavulla.
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 ”Vastausten suoratoisto ja ReadableStream-käsittelijöissä” 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 “Vastausten suoratoisto ja ReadableStream-käsittelijöissä”. 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 ”Vastausten suoratoisto ja ReadableStream-käsittelijöissä”?
Palauttakaa dataa vähitellen ReadableStreamilla esimerkiksi tekoälytokeneita, lokeja ja vaiheittain muodostuvia hyötykuormia varten. 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 3/4.
Kuinka kauan ”Vastausten suoratoisto ja ReadableStream-käsittelijöissä”-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
- RESTful-reittikäsittelijöiden suunnittelu Web Request APIlla
- Node-ajonaikainen ympäristö ja Edge-ajonaikainen ympäristö
- Vastausten suoratoisto ja ReadableStream-käsittelijöissä
- Pyyntöjen validointi ja tyypitetyt JSON-vastaukset Zodilla