前端 SSE 流式响应处理实践:从接收、解析到渲染

Server-Sent Events(SSE)是实现 AI 流式输出的最轻量方案。相比 WebSocket,它单向、基于 HTTP、浏览器原生支持,不需要额外库。

本文分享在"领航英语"项目中用 SSE 实现 AI 单词精讲的完整实践,包括前端接收、中断、结构化解析和逐行渲染。


为什么选 SSE

方案 适用场景 复杂度
轮询 低频更新
SSE 服务端单向推送流式数据
WebSocket 双向实时通信

AI 文本生成是典型的单向流式场景:用户发请求,AI 逐 token 返回。SSE 完美匹配。


后端接口(Go + Gin)

Go 侧设置关键响应头:

go 复制代码
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 API
        stream := llmClient.CreateChatCompletionStream(ctx, request)
        for chunk := range stream {
            fmt.Fprintf(w, "data: %s\n\n", chunk)
            c.Writer.Flush()
        }
        fmt.Fprintf(w, "data: [DONE]\n\n")
        return false
    })
}

关键细节:X-Accel-Buffering: no 告诉 Nginx 不要缓冲这个响应。没这行的话,Nginx 会把所有 chunk 攒到一起再发给客户端------流式变一次性,效果全没了。


前端 Fetch + ReadableStream

前端不用 EventSource API(因为它不支持 POST 请求和自定义 headers),用 fetch + ReadableStream

typescript 复制代码
async function streamAIExplain(word: string, onChunk: (text: string) => void) {
  const response = await fetch('/api/ai/explain-vocab/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ word })
  })

  const reader = response.body!.getReader()
  const decoder = new TextDecoder()
  let buffer = ''

  while (true) {
    const { done, value } = await reader.read()
    if (done) break

    buffer += decoder.decode(value, { stream: true })
    const lines = buffer.split('\n')
    buffer = lines.pop() || ''  // 最后一部分可能不完整,留着下次拼

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6)
        if (data === '[DONE]') return
        onChunk(data)
      }
    }
  }
}

要点:

  • TextDecoderstream: true 参数处理多字节字符被截断的情况(UTF-8 中一个中文字 3 字节,流式传输可能从中断开)
  • buffer 机制保证不完整的行不会丢
  • [DONE] 信号标记流结束

中断请求

用户关掉 AI 精讲面板时,需要立即中断请求,否则浪费 token 和带宽:

typescript 复制代码
const abortController = ref<AbortController | null>(null)

const startStream = async (word: string) => {
  abortController.value = new AbortController()

  const response = await fetch('/api/ai/explain-vocab/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ word }),
    signal: abortController.value.signal
  })
  // ...
}

const stopStream = () => {
  abortController.value?.abort()
}

AbortController 是浏览器原生 API,fetch 收到 abort 信号会抛出 AbortError,后端也会收到连接断开通知。


结构化 JSON 解析

AI 返回的不是纯文本,而是结构化 JSON:

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 累积,尝试解析,失败就继续收

typescript 复制代码
let jsonBuffer = ''

onChunk = (chunk: string) => {
  jsonBuffer += chunk
  let parsed: VocabExplain | null = null

  try {
    parsed = JSON.parse(jsonBuffer)
  } catch {
    return  // JSON 还不完整,继续等
  }

  // 解析成功,按字段渲染
  renderField('memoryTip', parsed.memoryTip)
  renderField('usage', parsed.usage)
  renderField('example', parsed.example)
  renderField('examNote', parsed.examNote)
}

生产环境建议用更鲁棒的策略:要求 LLM 逐字段输出,每个字段标记分隔符,避免等整个 JSON 收完才开始渲染。


逐行打字效果渲染

收到字段内容后,用 requestAnimationFrame 实现逐字渲染:

typescript 复制代码
const displayText = ref('')
let displayTimer: number | null = null

function animateText(fullText: string) {
  let index = 0
  const speed = 30  // ms per char

  const tick = () => {
    if (index < fullText.length) {
      displayText.value = fullText.slice(0, ++index)
      displayTimer = window.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 元。

相关推荐
程序大视界1 小时前
AI正在“接管“法槌?2026年法律AI全面入侵:合同审查99.2%准确率,律师该何去何从?
人工智能·ai法律
暗夜猎手-大魔王1 小时前
转载--Hermes Agent 12 | 沙箱与执行环境:六种终端后端的安全隔离
人工智能·安全
ylscode1 小时前
CISA紧急拉响警报:SolarWinds Serv-U曝高危漏洞CVE-2026-28318,零认证即可瘫痪文件传输服务
人工智能·安全
超人不会飞_Jay1 小时前
6.2前端笔记
前端·javascript·笔记
PythonFun1 小时前
WPS智能文档:解锁高效写作新体验
人工智能·wps
鹏大师运维1 小时前
统信UOS安装Subtitle Edit并使用Edge-TTS生成AI语音教程
linux·前端·人工智能·edge·麒麟·统信uos·ai语音
小赖同学啊1 小时前
基于MCP与主流AI技术架构 水利 发电 公园中的应用
人工智能·架构
morning_judger1 小时前
Agent开发系列(六)-安全护栏建设
人工智能·安全
程序员小羊!1 小时前
02CSS预备知识
前端·css3