Server-Sent EventsSSE是实现 AI 流式输出的最轻量方案。相比 WebSocket它单向、基于 HTTP、浏览器原生支持不需要额外库。本文分享在领航英语项目中用 SSE 实现 AI 单词精讲的完整实践包括前端接收、中断、结构化解析和逐行渲染。为什么选 SSE方案适用场景复杂度轮询低频更新低SSE服务端单向推送流式数据低WebSocket双向实时通信高AI 文本生成是典型的单向流式场景用户发请求AI 逐 token 返回。SSE 完美匹配。后端接口Go GinGo 侧设置关键响应头func(h*AIService)ExplainVocabStream(c*gin.Context){c.Header(Content-Type,text/event-stream)c.Header(Cache-Control,no-cache)c.Header(Connection,keep-alive)c.Header(X-Accel-Buffering,no)// 禁用 Nginx 缓冲c.Stream(func(w io.Writer)bool{// 调用 LLM streaming APIstream:llmClient.CreateChatCompletionStream(ctx,request)forchunk:rangestream{fmt.Fprintf(w,data: %s\n\n,chunk)c.Writer.Flush()}fmt.Fprintf(w,data: [DONE]\n\n)returnfalse})}关键细节X-Accel-Buffering: no告诉 Nginx 不要缓冲这个响应。没这行的话Nginx 会把所有 chunk 攒到一起再发给客户端——流式变一次性效果全没了。前端 Fetch ReadableStream前端不用 EventSource API因为它不支持 POST 请求和自定义 headers用fetchReadableStreamasyncfunctionstreamAIExplain(word:string,onChunk:(text:string)void){constresponseawaitfetch(/api/ai/explain-vocab/stream,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({word})})constreaderresponse.body!.getReader()constdecodernewTextDecoder()letbufferwhile(true){const{done,value}awaitreader.read()if(done)breakbufferdecoder.decode(value,{stream:true})constlinesbuffer.split(\n)bufferlines.pop()||// 最后一部分可能不完整留着下次拼for(constlineoflines){if(line.startsWith(data: )){constdataline.slice(6)if(data[DONE])returnonChunk(data)}}}}要点TextDecoder的stream: true参数处理多字节字符被截断的情况UTF-8 中一个中文字 3 字节流式传输可能从中断开buffer 机制保证不完整的行不会丢[DONE]信号标记流结束中断请求用户关掉 AI 精讲面板时需要立即中断请求否则浪费 token 和带宽constabortControllerrefAbortController|null(null)conststartStreamasync(word:string){abortController.valuenewAbortController()constresponseawaitfetch(/api/ai/explain-vocab/stream,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({word}),signal:abortController.value.signal})// ...}conststopStream(){abortController.value?.abort()}AbortController是浏览器原生 APIfetch 收到 abort 信号会抛出AbortError后端也会收到连接断开通知。结构化 JSON 解析AI 返回的不是纯文本而是结构化 JSON{memoryTip:词根 spect 表示看pro- 表示向前合起来就是向前看→前景,usage:prospect of doing sth, in prospect,example:The prospect of studying abroad excites her.,examNote:注意与 perspective视角区分考试中常混在一起出题}问题是SSE 的每个 chunk 可能在任何位置断开而 JSON 必须完整才能解析。解法——逐 chunk 累积尝试解析失败就继续收letjsonBufferonChunk(chunk:string){jsonBufferchunkletparsed:VocabExplain|nullnulltry{parsedJSON.parse(jsonBuffer)}catch{return// JSON 还不完整继续等}// 解析成功按字段渲染renderField(memoryTip,parsed.memoryTip)renderField(usage,parsed.usage)renderField(example,parsed.example)renderField(examNote,parsed.examNote)}生产环境建议用更鲁棒的策略要求 LLM 逐字段输出每个字段标记分隔符避免等整个 JSON 收完才开始渲染。逐行打字效果渲染收到字段内容后用requestAnimationFrame实现逐字渲染constdisplayTextref()letdisplayTimer:number|nullnullfunctionanimateText(fullText:string){letindex0constspeed30// ms per charconsttick(){if(indexfullText.length){displayText.valuefullText.slice(0,index)displayTimerwindow.requestAnimationFrame(()setTimeout(tick,speed))}}tick()}requestAnimationFrame保证渲染与屏幕刷新同步不会出现卡顿和闪烁。完整流程用户点击 AI 精讲 → fetch POST /api/ai/explain-vocab/stream → Go 服务调用 LLM streaming API → SSE chunk 逐条返回 → 前端累积 JSON buffer → 解析成功后逐字渲染 → 用户关闭面板 → AbortController 中断这些代码来自领航英语的实际实现。在线体验m.dobell.top点击任意单词卡片即可看到流式 AI 精讲效果。注册送 3 天会员月卡 29 元。
前端 SSE 流式响应处理实践:从接收、解析到渲染
发布时间:2026/6/6 8:59:33
Server-Sent EventsSSE是实现 AI 流式输出的最轻量方案。相比 WebSocket它单向、基于 HTTP、浏览器原生支持不需要额外库。本文分享在领航英语项目中用 SSE 实现 AI 单词精讲的完整实践包括前端接收、中断、结构化解析和逐行渲染。为什么选 SSE方案适用场景复杂度轮询低频更新低SSE服务端单向推送流式数据低WebSocket双向实时通信高AI 文本生成是典型的单向流式场景用户发请求AI 逐 token 返回。SSE 完美匹配。后端接口Go GinGo 侧设置关键响应头func(h*AIService)ExplainVocabStream(c*gin.Context){c.Header(Content-Type,text/event-stream)c.Header(Cache-Control,no-cache)c.Header(Connection,keep-alive)c.Header(X-Accel-Buffering,no)// 禁用 Nginx 缓冲c.Stream(func(w io.Writer)bool{// 调用 LLM streaming APIstream:llmClient.CreateChatCompletionStream(ctx,request)forchunk:rangestream{fmt.Fprintf(w,data: %s\n\n,chunk)c.Writer.Flush()}fmt.Fprintf(w,data: [DONE]\n\n)returnfalse})}关键细节X-Accel-Buffering: no告诉 Nginx 不要缓冲这个响应。没这行的话Nginx 会把所有 chunk 攒到一起再发给客户端——流式变一次性效果全没了。前端 Fetch ReadableStream前端不用 EventSource API因为它不支持 POST 请求和自定义 headers用fetchReadableStreamasyncfunctionstreamAIExplain(word:string,onChunk:(text:string)void){constresponseawaitfetch(/api/ai/explain-vocab/stream,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({word})})constreaderresponse.body!.getReader()constdecodernewTextDecoder()letbufferwhile(true){const{done,value}awaitreader.read()if(done)breakbufferdecoder.decode(value,{stream:true})constlinesbuffer.split(\n)bufferlines.pop()||// 最后一部分可能不完整留着下次拼for(constlineoflines){if(line.startsWith(data: )){constdataline.slice(6)if(data[DONE])returnonChunk(data)}}}}要点TextDecoder的stream: true参数处理多字节字符被截断的情况UTF-8 中一个中文字 3 字节流式传输可能从中断开buffer 机制保证不完整的行不会丢[DONE]信号标记流结束中断请求用户关掉 AI 精讲面板时需要立即中断请求否则浪费 token 和带宽constabortControllerrefAbortController|null(null)conststartStreamasync(word:string){abortController.valuenewAbortController()constresponseawaitfetch(/api/ai/explain-vocab/stream,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({word}),signal:abortController.value.signal})// ...}conststopStream(){abortController.value?.abort()}AbortController是浏览器原生 APIfetch 收到 abort 信号会抛出AbortError后端也会收到连接断开通知。结构化 JSON 解析AI 返回的不是纯文本而是结构化 JSON{memoryTip:词根 spect 表示看pro- 表示向前合起来就是向前看→前景,usage:prospect of doing sth, in prospect,example:The prospect of studying abroad excites her.,examNote:注意与 perspective视角区分考试中常混在一起出题}问题是SSE 的每个 chunk 可能在任何位置断开而 JSON 必须完整才能解析。解法——逐 chunk 累积尝试解析失败就继续收letjsonBufferonChunk(chunk:string){jsonBufferchunkletparsed:VocabExplain|nullnulltry{parsedJSON.parse(jsonBuffer)}catch{return// JSON 还不完整继续等}// 解析成功按字段渲染renderField(memoryTip,parsed.memoryTip)renderField(usage,parsed.usage)renderField(example,parsed.example)renderField(examNote,parsed.examNote)}生产环境建议用更鲁棒的策略要求 LLM 逐字段输出每个字段标记分隔符避免等整个 JSON 收完才开始渲染。逐行打字效果渲染收到字段内容后用requestAnimationFrame实现逐字渲染constdisplayTextref()letdisplayTimer:number|nullnullfunctionanimateText(fullText:string){letindex0constspeed30// ms per charconsttick(){if(indexfullText.length){displayText.valuefullText.slice(0,index)displayTimerwindow.requestAnimationFrame(()setTimeout(tick,speed))}}tick()}requestAnimationFrame保证渲染与屏幕刷新同步不会出现卡顿和闪烁。完整流程用户点击 AI 精讲 → fetch POST /api/ai/explain-vocab/stream → Go 服务调用 LLM streaming API → SSE chunk 逐条返回 → 前端累积 JSON buffer → 解析成功后逐字渲染 → 用户关闭面板 → AbortController 中断这些代码来自领航英语的实际实现。在线体验m.dobell.top点击任意单词卡片即可看到流式 AI 精讲效果。注册送 3 天会员月卡 29 元。