
目录一、SSE 是什么二、SSE 用来干什么三、底层 HTTP 原理3.1 请求头客户端 → 服务器3.2 响应头服务器 → 客户端3.3 交互过程全生命周期3.4 消息格式四、Vue3 FastAPI 实战4.1 核心思路4.2 后端FastAPI 返回 SSE 流4.3 前端GET 场景4.4 前端POST 场景fetch ReadableStream 手动解析一、SSE 是什么SSEServer-Sent Events是一种基于 HTTP 的服务器推送技术。它允许服务器通过一条持久的 HTTP 连接持续地向客户端单向推送数据流。核心特征就一句话客户端发起一次请求服务器可以多次响应。与 WebSocket 的双向全双工不同SSE 是单向的——数据只从服务器流向客户端。客户端如果需要发送数据需要另外发 HTTP 请求。二、SSE 用来干什么SSE服务器推送事件实现服务端主动单向向浏览器持续下发数据无需客户端反复轮询。场景说明AI 对话流式输出ChatGPT 那种逐字输出的效果SSE 是最佳选择。实时通知服务器推送系统通知、告警。实时数据看板股票行情、服务器监控指标。日志流实时推送服务器日志到前端。进度条文件上传/处理进度实时反馈。三、底层 HTTP 原理SSE 不是魔改协议它就是标准 HTTP只不过连接不断开。3.1 请求头客户端 → 服务器客户端发起的是一个普通的 GET 请求但带了一个关键请求头GET /events HTTP/1.1 Host: example.com Accept: text/event-stream Cache-Control: no-cacheAccept: text/event-stream告诉服务器我期望接收 SSE 事件流。Cache-Control: no-cache禁止中间代理缓存确保数据实时性。指示所有中间缓存节点如代理服务器、CDN在处理请求时不要走自身缓存而是将请求转发给源站以便获取最新数据。就这么简单。没有 Upgrade 握手没有协议切换。3.2 响应头服务器 → 客户端服务器收到请求后返回如下响应头HTTP/1.1 200 OK Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive Transfer-Encoding: chunked响应头含义Content-Type: text/event-stream核心。告诉浏览器这不是普通响应而是 SSE 事件流。浏览器据此触发EventSource的解析逻辑。Cache-Control: no-cache禁止缓存。SSE 是实时数据缓存没有意义。Connection: keep-alive保持 TCP 连接不断开。Transfer-Encoding: chunked分块传输编码。服务器不知道内容总长度因为流是持续的所以用 chunked 模式每次推送一小块数据。3.3 交互过程全生命周期客户端 服务器 | | |--- GET /events -------------| ① 客户端发起请求 | Accept: text/event-stream | | | |--- 200 OK -----------------| ② 服务器返回响应头连接建立 | Content-Type: text/event-stream | | |--- data: {msg: hello} -| ③ 服务器推送第1条消息 | | |--- data: {msg: world} -| ④ 服务器推送第2条消息 | | |--- data: {msg: ...} ---| ⑤ 持续推送... | | |--- 自动重连 ----------------| ⑥ 连接断开后浏览器自动重连 | |关键点连接建立后不会关闭。TCP 连接一直保持服务器随时可以写入数据。浏览器自动重连。如果连接断开EventSource默认会在几秒后自动重连无需手动处理。Last-Event-ID。重连时浏览器会自动带上Last-Event-ID请求头告诉服务器我上次收到的事件 ID 是什么服务器可以据此做断点续传。3.4 消息格式SSE 的 body 是纯文本有严格的格式规范每条消息核心数据以data:开头后面跟内容。消息之间用两个换行符\n\n分隔。内容可以是纯文本、JSON任意格式。data: {temperature: 23.5} data: 这是第一条消息 data: 这是第二条消息每条消息除了必须有data也可以有其他几个扩展字段可选一条消息的多个字段之间用\n隔离。id: 42 event: temperature data: {value: 23.5} retry: 5000id事件 ID用于重连时的Last-Event-ID。event自定义事件类型前端可以用addEventListener(temperature, ...)监听。retry告诉浏览器重连间隔毫秒。四、Vue3 FastAPI 实战4.1 核心思路整个实战围绕两个关键决策展开后端用什么返回 SSE—— FastAPI 的StreamingResponse它底层走Transfer-Encoding: chunked天然适配 SSE 的连接不断开、数据分批写模式。前端怎么接收 SSE—— 分两种情况GET 场景用浏览器原生的EventSource零依赖、自动重连POST 场景用fetchReadableStream手动解析因为EventSource只支持 GET 且无法自定义请求头当然还有请求体。下面逐一展开。4.2 后端FastAPI 返回 SSE 流先看一个最简示例——模拟一个任务进度推送importasyncioimportjsonfromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponse appFastAPI()asyncdefevent_generator():SSE 事件生成器——这是整个后端的核心messages[正在分析数据...,查询数据库...,生成报告...,任务完成!]fori,msginenumerate(messages):awaitasyncio.sleep(1)# 模拟耗时操作yieldfid:{i}\nyieldfdata:{json.dumps({message:msg,progress:(i1)/len(messages)*100},ensure_asciiFalse)}\n\napp.get(/api/stream)asyncdefsse_stream():returnStreamingResponse(event_generator(),media_typetext/event-stream,headers{Cache-Control:no-cache,Connection:keep-alive,X-Accel-Buffering:no,# 禁用 Nginx 缓冲},)三个关键点逐一说清楚①StreamingResponse是 FastAPI 做 SSE 的唯一正确方式。它接受一个异步生成器每次yield出一块数据FastAPI 就用 chunked 模式发给客户端。普通JSONResponse会等所有数据就绪才一次性返回流式效果就没了。②media_typetext/event-stream是灵魂。没有这个 Content-Type浏览器不会把响应当成 SSE 流来解析EventSource的onmessage永远不会触发。这是整个 SSE 协议最核心的一个响应头。③yield的格式必须严格遵守data: xxx\n\n。每条消息以data:开头、以两个换行符结尾。格式错一个字符前端都可能解析不出来。id和event字段是可选的但data和结尾的\n\n是必须的。补充X-Accel-Buffering: no是专门给 Nginx 看的。如果你的服务直接暴露给客户端、不经过 Nginx 代理这个头可以省略。但生产环境通常都会过一层 Nginx加上它确保 Nginx 不缓冲响应。4.3 前端GET 场景当 SSE 接口是 GET 请求、不需要传自定义 Header如Authorization时浏览器原生的EventSource就是最佳选择EventSource是浏览器专门为 SSE 封装好的高层 API但先天限制仅 GET、不能自定义请求体// composables/useSSE.tsimport{ref,onUnmounted}fromvue;exportfunctionuseSSE(url:string){constdatarefany(null);constconnectedref(false);letes:EventSource|nullnull;functionconnect(){esnewEventSource(url);es.onopen()(connected.valuetrue);// 监听默认事件data 不带 event 字段的消息es.onmessage(e){data.valueJSON.parse(e.data);};// 监听自定义事件event: temperature 的消息es.addEventListener(temperature,(e:MessageEvent){console.log(温度:,JSON.parse(e.data));});es.onerror(){connected.valuefalse;// 无需手动重连——EventSource 默认 3 秒后自动重连};}functionclose(){es?.close();connected.valuefalse;}onUnmounted(close);return{data,connected,connect,close};}用法的精髓在于event字段后端发的消息如果不带event字段 → 前端用onmessage接收。后端如果带了event: temperature→ 前端用addEventListener(temperature, ...)接收。这意味着一个 SSE 连接可以承载多种类型的消息前端按事件名分发到不同的处理逻辑就像频道一样。EventSource的局限也很明确只能发 GET不能带自定义请求头。如果你的接口需要 POST 传参或者要带Authorization: Bearer xxxEventSource就无能为力了。这时候需要换一种方式。4.4 前端POST 场景fetch ReadableStream 手动解析AI 对话流式输出是 SSE 最经典的应用场景但它有一个问题发送用户消息通常用 POST消息体长、参数复杂不适合塞 URL 里。EventSource不支持 POST所以只能改用fetch手动读取响应体中的 SSE 流。// composables/useChatSSE.tsimport{ref}fromvue;exportfunctionuseChatSSE(){constresponseref();constisStreamingref(false);asyncfunctionsendMessage(message:string){response.value;isStreaming.valuetrue;constresawaitfetch(/api/chat,{method:POST,headers:{Content-Type:application/json},body:JSON.stringify({message}),});constreaderres.body!.getReader();// 拿到可读流constdecodernewTextDecoder();// 字节 → 文本letbuffer;while(true){const{done,value}awaitreader.read();if(done)break;bufferdecoder.decode(value,{stream:true});// 按 \n\n 切分消息最后一段可能不完整留到下次constpartsbuffer.split(\n\n);bufferparts.pop()||;for(constpartofparts){if(part.startsWith(data: )){constjsonJSON.parse(part.slice(6));if(json.done){isStreaming.valuefalse;}else{response.valuejson.text;}}}}isStreaming.valuefalse;}return{response,isStreaming,sendMessage};}这段代码的核心逻辑分三步理解fetch发出 POST 请求拿到Response对象。注意这里不能await res.json()——那样会等全部数据到达才解析流式就废了。必须用res.body.getReader()拿到底层的数据流读取器。循环reader.read()每次读到一个数据块。服务器推送一块read()就返回一块。done为true表示流结束。TextDecoder负责把Uint8Array字节转成可读的字符串。按\n\n分割解析data:行。SSE 消息之间用\n\n分隔但由于网络传输是按块到达的一个块可能包含半条消息所以用buffer缓存不完整的尾部下次循环再拼接。对比两种前端方案EventSource胜在简单——自动重连、自动解析零心智负担但它只能 GET。fetchReadableStream更灵活——支持 POST、自定义 Header、甚至可以中途abort()取消请求但需要手动处理 SSE 协议格式和重连逻辑。实际项目中进度推送、通知类场景用EventSourceAI 对话、需要传参的场景用fetch。