很多同学已经熟练使用 AI 辅助编程了,但直觉在本地搭建一个轻便的 SSE "打字"输出的效果并不好弄。有很多教程需要依赖 Express
或 Node
来监听端口,或者使用 Mock Service Worker (MSW)
,总之都要依赖点什么,一旦中间某处卡住了,直接"未开始便放弃了"。其实,只需要 Next.js 就够了。

框架与依赖
框架
Next.js (支持 App router 的版本)
依赖(非必须)
@microsoft/fetch-event-source
(直接使用原生的 EventSource
也可以)
思路
用 Next.js 中的 API route 模拟一个 text/event-stream
头的返回
代码
API route 部分
js
// 存到这里 src/app/api/sse/route.ts
const EVENTS = {
MESSAGE: "message",
ERROR: "error",
DONE: "done",
}
const WHEN_ERROR = 5;
const WHEN_DONE = 10;
export async function GET() {
const encoder = new TextEncoder();
let interval: NodeJS.Timeout;
const stream = new ReadableStream({
start(controller) {
let count = 0;
let payload;
interval = setInterval(() => {
count++;
payload = {
time: new Date().toISOString(),
count,
event: count === WHEN_DONE ? EVENTS.DONE : EVENTS.MESSAGE
};
// uncomment below lines to test error
// if (count === WHEN_ERROR) {
// payload.event = EVENTS.ERROR;
// }
// replace data in chunk with your split contents
const chunk = `id: ${payload.count}\nevent: ${payload.event}\ndata: ${JSON.stringify(payload)}\n\n`;
controller.enqueue(encoder.encode(chunk));
if ([EVENTS.ERROR,EVENTS.DONE].includes(payload.event)) {
clearInterval(interval);
controller.close();
}
}, 500);
},
cancel() {
// cleanup when the stream is canceled
clearInterval(interval);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
// 'X-Accel-Buffering': 'no', // helpful for some proxies
},
});
}
页面部分
js
// 存到这里 src/app/page.tsx
import { useEffect, useState } from "react";
import { fetchEventSource } from '@microsoft/fetch-event-source';
// 此处代码省略
useEffect(()=>{
const controller = new AbortController();
const { signal } = controller;
const getStream = async()=>{
console.log("SSE starts")
await fetchEventSource('/api/sse', {
signal,
async onopen(response) {
if (response.ok) {
console.log("SSE connected")
return;
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// client-side errors are usually non-retriable
throw new Error("SSE FatalError");
} else {
throw new Error("SSE RetriableError");
}
},
onmessage(ev) {
const data = JSON.parse(ev.data);
console.log("SSE data: ", data);
if (data.event === "error" ) {
controller.abort();
console.log("SSE error");
}
if (data.event === "done" ) {
console.log("SSE done");
}
if (["done", "error"].includes(data.event)) {
console.log("SSE closed");
return;
}
// setMessages(prev => [...prev,data])
},
onerror(err){
console.log("SSE error: ", err)
}
});
};
getStream();
}, []);
// 此处代码省略
结果
跑起来后,如果看到以下 console.log
就成功了。 Have fun :)
