前端对接 SSE 的两种常见方式
前端对接 SSE 的两种常见方式LLM 流式输出、进度推送、长任务状态更新后端经常会用SSEServer-Sent Events一条 HTTP 长连接服务端持续往下推事件客户端边收边渲染。协议本身不复杂事件大致长这样data: {event:content,data:你好} data: {event:end,traceId:abc}关键点响应头是Content-Type: text/event-stream一条事件通常以空行\n\n结束业务数据多放在data:后面常见再包一层 JSON前端真正要选的是怎么连上这条流。常规有两种EventSource浏览器原生fetchReadableStream手动读流一句话对比EventSourcefetch ReadableStream怎么连new EventSource(url)fetch(url)后读response.bodyHTTP 方法基本只有GETGET / POST / PUT…都行自定义请求头基本不行难带Authorization随便带请求体没有可以发 JSON body自动重连浏览器自带要自己写解析成本低浏览器帮你拆事件要自己按行/按段解析适合场景公开订阅、简单通知要登录、要 POST、要精细控制业务 API 往往需要Bearer Token POST body所以第二种更常见监控面板、公开进度页第一种更省事。方式一EventSource基本用法constesnewEventSource(/api/notifications/stream);es.onmessage(event){// event.data 就是 data: 后面的字符串constpayloadJSON.parse(event.data);console.log(payload);};es.onerror(){// 默认会自动重连不需要时可 es.close()console.error(SSE error);};// 主动断开// es.close();如果服务端用了命名事件event: progress可以这样听es.addEventListener(progress,(event){constpayloadJSON.parse((eventasMessageEvent).data);console.log(payload);});优点API 短上手快断线自动重连对「订阅型」推送很友好不用自己处理字节流和粘包限制也是很多人最终换掉它的原因基本只能 GET复杂任务参数不好塞进 URL还可能暴露在日志/代理里。很难带自定义 Header标准EventSource不能方便地加Authorization: Bearer token于是常见歪招是把 token 塞进 query?access_token...既丑也不安全。错误与状态不好细控HTTP 401/403、业务error事件、主动取消都不如fetch直观。什么时候用它不需要登录或鉴权已靠 Cookie同源自动带上接口本身就是 GET 订阅你需要浏览器自带的断线重连方式二fetchReadableStream思路用fetch发起请求可 POST、可带 Header用response.body.getReader()读二进制块TextDecoder转成文本按 SSE 规则拆出data:行JSON.parse后分发给业务回调发起带鉴权的 SSE 请求asyncfunctionrequestAuthorizedSse(url:string,init:{method:string;body?:string;signal?:AbortSignal},onDataLine:(dataLine:string)void){consttokenlocalStorage.getItem(token)||;constresponseawaitfetch(url,{method:init.method,headers:{Content-Type:application/json,Authorization:Bearer${token},},body:init.body,signal:init.signal,});if(!response.ok){consterrorBodyawaitresponse.text().catch(());thrownewError(errorBody||SSE request failed:${response.statusText});}awaitreadSseSegments(response,onDataLine,init.signal);}按「空行分段」解析推荐SSE 一条事件以\n\n结束按段切最稳asyncfunctionreadSseSegments(response:Response,onDataLine:(dataLine:string)void,signal?:AbortSignal){if(!response.body){thrownewError(SSE response has no body);}constreaderresponse.body.getReader();constdecodernewTextDecoder();letbuffer;try{while(true){if(signal?.aborted){thrownewDOMException(Aborted,AbortError);}const{done,value}awaitreader.read();if(done)break;bufferdecoder.decode(value,{stream:true});constsegmentsbuffer.split(\n\n);buffersegments.pop()||;for(constsegmentofsegments){consttrimmedsegment.trim();if(!trimmed.startsWith(data:))continue;constdataParttrimmed.replace(/^data:\s*/,);if(dataPart)onDataLine(dataPart);}}}finally{reader.releaseLock?.();}}业务侧把data解析成事件awaitrequestAuthorizedSse(/api/generate/draft,{method:POST,body:JSON.stringify({projectId,task}),signal:abortController.signal,},(dataLine){try{constdataJSON.parse(dataLine);switch(data.event){casestart:console.log(开始,data.traceId);break;casecontent:appendText(data.data);// 打字机效果break;caseend:finish(data);break;caseerror:showError(data.data);break;}}catch{// 忽略半包/脏行}});取消流AbortControllerconstabortControllernewAbortController();// 用户点「停止生成」abortController.abort();把signal传给fetch并在reader.read()循环里检查signal.aborted就能干净停掉。优点支持 POST JSON body复杂生成任务很常见能带Authorization等自定义头取消、超时、非 2xx 错误处理都更可控和现有 API Client 风格容易统一代价要自己处理粘包、半包、解码下一节展开没有浏览器那种「断了自动重连」需要的话得自己补粘包、半包、解码到底怎么处理reader.read()每次给你的不是「一条完整 SSE 事件」而是一块块字节Uint8Array。网络怎么切包你控制不了所以会出现三种情况。1解码字节 → 文本TCP/HTTP 流里先是二进制。中文等多字节字符还可能被拆到两次read()中间。constdecodernewTextDecoder();// stream: true 很重要告诉解码器「后面可能还有字节」// 遇到半个汉字时先缓存等下次凑齐再吐出完整字符bufferdecoder.decode(value,{stream:true});如果写成decoder.decode(value)默认stream: false半个 UTF-8 字符可能直接变成 或乱码。2半包一次read()不够一条事件服务端本意推送data: {event:content,data:你好}\n\n但第一次可能只收到data: {event:content,da第二次才收到ta:你好}\n\n如果每次read()立刻JSON.parse第一次必炸。做法先塞进buffer只处理已经完整的部分。3粘包一次read()塞了多条事件也可能一次就收到data: {event:start}\n\n data: {event:content,data:你}\n\n data: {event:content,data:好}\n\n如果只当一条处理会漏事件或解析失败。做法用分隔符切开循环处理每一段。4标准解法缓冲区 分隔符SSE 一条事件以空行\n\n结束所以每次 read 到一块字节 → decode 成文本追加到 buffer → 用 \n\n split → 最后一段多半是「还没收完的半包」塞回 buffer → 前面那些完整段再提取 data: 交给业务对应代码核心就三行bufferdecoder.decode(value,{stream:true});constsegmentsbuffer.split(\n\n);buffersegments.pop()||;// 半包留下完整段拿去处理图示buffer 当前内容 ┌─────────────────────────────────────────────┐ │ data: {event:start}\n\n │ ← 完整可处理 │ data: {event:content,data:你}\n\n │ ← 完整可处理 │ data: {event:cont │ ← 半包留在 buffer └─────────────────────────────────────────────┘ ↑ segments.pop() 留着等下次业务层JSON.parse再包一层try/catch是为了兜住脏数据真正防半包的是上面的 buffer不是 catch。服务端要配合什么无论前端用哪种连法服务端都要先把响应变成 SSEres.writeHead(200,{Content-Type:text/event-stream,Cache-Control:no-cache,Connection:keep-alive,X-Accel-Buffering:no,// 避免 Nginx 把流缓冲住});// 可选先写一行注释心跳帮部分代理保持连接res.write(: keep-alive\n\n);// 推一条业务事件res.write(data:${JSON.stringify({event:content,data:你好})}\n\n);// 结束res.end();客户端断开时记得停掉后续写入req.on(close,(){abortedtrue;});为什么常说「拿原生 res 自己写别走普通 JSON 拦截器」普通接口的返回路径通常是Controller return { foo: 1 } → 拦截器 / 管道再包一层 → 变成 { code: 0, msg: success, data: { foo: 1 } } → 框架一次性 JSON.stringify 后发给前端SSE 要的是另一条路先写响应头 Content-Type: text/event-stream → 每隔一会儿 res.write(data: ...\n\n) → 连接一直开着最后再 res.end()如果 SSE 也走「普通 JSON 拦截器」常见会坏在三处格式被包坏你本想推data: {event:content,data:你好}\n\n拦截器却可能变成一整段{code:0,msg:success,data:……流内容或对象……}前端按 SSE 去拆data:行全对不上。时机不对JSON 接口是「算完再一次性返回」。SSE 是「边算边推」。拦截器等你return才包装流式体验没了。Content-Type 不对普通接口默认application/jsonSSE 必须是text/event-stream。头设错了浏览器/客户端不会按事件流处理。所以 Nest 里常见写法是Post(optimize/plan)asyncoptimizePlan(Req()req,Res()res){// Res()接管原生响应框架不再替你自动 JSON.stringifyres.writeHead(200,{Content-Type:text/event-stream,/* ... */});res.write(data:${JSON.stringify({event:start})}\n\n);// ... 持续 writeres.end();}如果项目有全局响应拦截器还要对 SSE跳过包装例如看到已经是text/event-stream就原样放过// 伪代码全局拦截器里if(contentType.includes(text/event-stream)){returnnext.handle();// 不要 map 成 { code, msg, data }}returnnext.handle().pipe(map((data)({code:0,msg:success,data})));一句话普通接口框架帮你打包成 JSON 信封。SSE你自己按事件协议往响应里「一点一点写」别让信封逻辑插手。怎么选需要 POST body或需要 Authorization Header ├─ 是 → fetch ReadableStream └─ 否 ├─ 需要自动重连的简单订阅 → EventSource └─ 仍想统一客户端封装 → 也可以一律用 fetch实战经验聊天/写作/长任务生成几乎都是第二种公告、公开看板、简单通知第一种够用团队若已有鉴权 API Client优先第二种少维护两套连接哲学小结SSE 是服务端推事件的 HTTP 长连接核心格式是data: ...\n\n方式一EventSource简单、能自动重连但基本限于 GET难带自定义头方式二fetch ReadableStream可 POST、可带 Token、可 Abort解析要自己写粘包/半包靠buffer \n\n分隔解码用TextDecoder({ stream: true })SSE 不要走普通 JSON 信封拦截器自己writeHead 持续write选哪种看你的接口要不要鉴权和请求体大多数业务流式接口会选第二种

相关新闻

深度剖析Nova的存档机制:实现无缝回溯与多结局设计

深度剖析Nova的存档机制:实现无缝回溯与多结局设计

深度剖析Nova的存档机制:实现无缝回溯与多结局设计 【免费下载链接】Nova Programmer-friendly framework for visual novels (VN) / text-based adventure games (AVG) on Unity 项目地址: https://gitcode.com/gh_mirrors/nova1/Nova Nova作为一款面向程序…

2026/10/3 21:09:45 阅读更多 →
从 0 搭一个 AI Agent:用 LangChain + MCP 让大模型自己调用工具完成任务

从 0 搭一个 AI Agent:用 LangChain + MCP 让大模型自己调用工具完成任务

平时用大模型,我们是一问一答,它只动嘴不动手。但有些活其实可以让它自己去找资料、调接口、跑命令,比如"查一下杭州明后天的天气,再帮我订个提醒"。这种能自己动手干活的模型,就叫 Agent(智能体…

2026/10/12 6:46:38 阅读更多 →
米利珠单抗Mirikizumab治疗溃疡性结肠炎,每季度一针让肠道重获安宁

米利珠单抗Mirikizumab治疗溃疡性结肠炎,每季度一针让肠道重获安宁

溃疡性结肠炎是一种慢性复发性肠道炎症,患者的结肠和直肠黏膜持续发炎、糜烂,表现为反复腹泻、黏液脓血便、腹痛和里急后重。中重度患者每天腹泻可达10次以上,夜间也要频繁起床上厕所,生活质量严重受损。传统治疗依赖氨基水杨酸、…

2026/10/7 10:27:11 阅读更多 →

最新新闻

Python语音识别实战:从MFCC特征提取到CTC训练与避坑指南

Python语音识别实战:从MFCC特征提取到CTC训练与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 6:45:56 阅读更多 →
从单Agent到多Agent集群:7x24小时稳定运行的架构设计与实践

从单Agent到多Agent集群:7x24小时稳定运行的架构设计与实践

半夜两点二十三分,我的单 Agent 自动化流程终于把自己玩死了:上下文窗口撑爆之后,它开始对着同一个任务反复"确认",把状态标记改得乱七八糟,任务队列里还堆着两百多条没处理的数据。那一刻我意识到&#xff…

2026/10/12 6:45:56 阅读更多 →
旗帜图片爬虫实战:格式校验、感知哈希去重与增量更新指南

旗帜图片爬虫实战:格式校验、感知哈希去重与增量更新指南

简介:这是一份面向Python爬虫入门学习者与数据分析初学者的实战源码包,以抓取东京奥运会奖牌国家及地区旗帜图片为案例,演示从网页解析、图片下载到数据存储的完整流程。压缩包内含两个文件——一个HTML文件和一个Jupyter Notebook文件&#…

2026/10/12 6:45:56 阅读更多 →
双 11 Token 预算红线硬限制:当单日模型消耗达到 80% 阈值时的自适应限流

双 11 Token 预算红线硬限制:当单日模型消耗达到 80% 阈值时的自适应限流

去年大促预售第一天中午,财务总监踩着高跟鞋直接冲进了我们技术架构组,脸色铁青。她指着大模型供应商后台的账单明细:公司账户里预先充值的 15 万元 Token 额度,从凌晨 0 点到中午 11 点半,短短不到半天时间被烧掉了 1…

2026/10/12 6:45:56 阅读更多 →
大模型私有化部署:从硬件选型到安全合规的全栈生产实践

大模型私有化部署:从硬件选型到安全合规的全栈生产实践

1. 私有化部署不是“把模型拷贝过去”,而是重建一套生产级推理基础设施很多人第一次听说“大模型私有化部署”,脑子里浮现的画面是:下载一个GGUF格式的模型文件,丢进某个本地GUI工具里点几下,然后弹出个聊天窗口——事…

2026/10/12 6:45:56 阅读更多 →
开源大模型私有化部署实战:算力底座、显存规划与推理框架选型

开源大模型私有化部署实战:算力底座、显存规划与推理框架选型

开源模型私有化部署从来不是一件“装个软件就能跑”的事。我接手过不少内部演示和实验项目,最深的体会是:模型本身的参数再漂亮,落到自己的机器上、接到业务流程里,才是真正见功夫的地方。这段时间圈子里对开源模型的讨论很多&…

2026/10/12 6:44:55 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →