阶段六:SSE AI 流式对话
从零构建带AI流式对话的Vue2全栈应用-CSDN博客本文是对这篇文章阶段六的讲解一、普通接口为什么不能产生打字机效果普通接口用户提问 ↓ 后端生成完整回答 ↓ 一次性返回整个回答 ↓ 页面一次性显示SSE用户提问 ↓ 后端保持连接 ↓ 发送第一个字 ↓ 发送第二个字 ↓ 发送第三个字 ↓ 直到结束这就是类似 ChatGPT 的流式输出。二、SSE 是什么SSE 全称Server-Sent Events意思是服务器持续向浏览器发送事件。它主要是单向通信服务器 → 浏览器浏览器使用new EventSource(url)连接服务器。三、SSE 后端响应头res.setHeader( Content-Type, text/event-stream; charsetutf-8 )告诉浏览器这是 SSE 数据而不是普通 JSON。res.setHeader( Cache-Control, no-cache, no-transform )禁止缓存和修改流式内容。res.setHeader( Connection, keep-alive )告诉网络连接不要立刻关闭。res.setHeader( X-Accel-Buffering, no )避免部分代理服务器把数据缓存起来等全部完成后才一次性返回。res.flushHeaders()立即发送响应头。不需要等待第一段正文。四、SSE 消息格式后端res.write( data: ${JSON.stringify(data)}\n\n )SSE 格式要求data: 数据内容每条消息后必须有两个换行。代码中的\n\n就是两个换行。发送sendEvent({ type: content, text: 你 })浏览器收到data: {type:content,text:你}五、为什么要定义不同的事件类型我们自己设计了三种消息{ type: start }表示开始生成。{ type: content, text: 你 }表示新生成了一段文字。{ type: end }表示生成结束。还可以有{ type: error, message: 模型调用失败 }表示错误。六、模拟逐字发送const characters Array.from(answer)把字符串变成字符数组。例如Array.from(你好)得到[你, 好]然后let currentIndex 0记录当前发送到第几个字符。setIntervalconst timer setInterval(() { // 每隔 70 毫秒执行一次 }, 70)每隔 70 毫秒发送一个字符。第一次你第二次好第三次继续。这就产生了打字机效果。发送完成if ( currentIndex characters.length ) { sendEvent({ type: content, text: characters[currentIndex] }) currentIndex 1 return }如果还有字符就发送当前字符。全部发送完成后sendEvent({ type: end }) clearInterval(timer) res.end()clearInterval停止定时器。res.end()关闭 HTTP 响应。七、用户停止生成前端执行this.eventSource.close()浏览器关闭 SSE 连接。后端监听req.on(close, () { clearInterval(timer) })连接关闭后停止继续生成。真实大模型时还应该同时取消上游模型请求避免继续产生费用。八、为什么 SSE 使用EventSource不用 AxiosAxios 更适合普通请求发送请求 等待完整响应 得到结果SSE 需要连接持续存在 不断接收新消息浏览器原生提供EventSource所以使用this.eventSource new EventSource(streamUrl)九、聊天页面中的数据data() { return { question: , messages: [], streamingContent: , isStreaming: false, eventSource: null, nextMessageId: 1 } }questionquestion: 保存输入框内容。模板el-input v-modelquestion /v-model是双向绑定。用户输入文字 → question 变化同时question 变化 → 输入框显示变化messagesmessages: []保存已经完成的历史消息。例如[ { id: 1, role: user, content: 你好 }, { id: 2, role: assistant, content: 你好请问有什么可以帮助你 } ]streamingContentstreamingContent: 保存当前还在生成中的 AI 回答。收到一个字this.streamingContent data.text例如原来你 新字符好 结果你好isStreamingisStreaming: false表示是否正在生成。false可以发送新问题 true正在生成显示停止按钮模板可以根据它切换按钮el-button v-ifisStreaming 停止生成 /el-button el-button v-else 发送 /el-buttoneventSourceeventSource: null保存当前 SSE 连接对象。有了它才能主动执行this.eventSource.close()十、发送问题流程sendQuestion() { const trimmedQuestion this.question.trim() if ( !trimmedQuestion || this.isStreaming ) { return } }先去掉空格。如果问题为空直接结束。如果正在生成也直接结束避免同时建立多个连接。保存用户消息this.messages.push({ id: this.nextMessageId, role: user, content: trimmedQuestion })向数组添加一条用户消息。Vue 发现数组变化后会自动更新页面中的v-formessage in messages清空输入框this.question 因为输入框使用v-modelquestion所以数据变空后输入框也自动清空。建立 SSEconst streamUrl createAiStreamUrl( trimmedQuestion ) this.eventSource new EventSource(streamUrl)最终地址类似/api/ai/stream?question请介绍一下SSE十一、onmessagethis.eventSource.onmessage event { const data JSON.parse(event.data) }每次服务器发送data: ...都会触发onmessage。event.data是字符串。例如{type:content,text:你}通过JSON.parse(event.data)转换成 JavaScript 对象。处理文字if (data.type content) { this.streamingContent data.text || return }每收到一个文字片段就追加到页面。处理结束if (data.type end) { this.finishStream() }finishStream()会把临时回答保存到 messages 关闭 SSE 恢复发送按钮十二、保存流式消息saveStreamingMessage() { if (!this.streamingContent) { return } this.messages.push({ id: this.nextMessageId, role: assistant, content: this.streamingContent }) this.nextMessageId 1 this.streamingContent }生成过程中回答在streamingContent生成完成后转移到messages然后把临时内容清空。十三、为什么要在beforeDestroy关闭连接beforeDestroy() { this.closeEventSource() }当用户离开聊天页面时组件准备销毁。如果不关闭 SSE用户已经进入别的页面 后端仍然继续发送 连接仍占用资源所以离开页面前必须关闭。十四、自动滚动scrollToBottom() { this.$nextTick(() { const container this.$refs.messageContainer container.scrollTop container.scrollHeight }) }$refs模板中div refmessageContainer脚本中可以通过this.$refs.messageContainer获得这个真实 HTML 元素。scrollHeight表示内容总高度。scrollTop表示当前滚动位置。设置scrollTop scrollHeight就会滚动到底部。$nextTickVue 修改数据后真实网页不是立即同步完成。所以先等待 Vue 更新 DOMthis.$nextTick(...)再滚动。十五、为什么阶段五 Cookie 对 SSE 很重要原生EventSource不方便添加自定义请求头。很难像 Axios 一样手动写Authorization: Bearer ...但是浏览器会自动给同源 EventSource 请求携带 Cookie。所以组合是阶段五HttpOnly Cookie 阶段六EventSource后端requireAuth可以正常读取 Cookie判断用户是否登录。

相关新闻

阶段五:HttpOnly Cookie 登录持久化

阶段五:HttpOnly Cookie 登录持久化

从零构建带AI流式对话的Vue2全栈应用-CSDN博客本文是对这篇文章阶段五的讲解 一、阶段五为什么要改 Token 保存方式 阶段四: Token 保存在 Vuex 前端 JavaScript 可以读取 Token 刷新后 Token 消失 阶段五: Token 保存在 HttpOnly Cookie JavaScrip…

2026/7/19 14:49:04阅读更多 →
番茄钟终极指南:如何用Tomodoro实现深度专注与高效时间管理

番茄钟终极指南:如何用Tomodoro实现深度专注与高效时间管理

番茄钟终极指南:如何用Tomodoro实现深度专注与高效时间管理 【免费下载链接】tomodoro A pomodoro web app with PIP mode, white noise generation, tasks and more! 项目地址: https://gitcode.com/gh_mirrors/to/tomodoro 你是否曾坐在电脑前数小时&#…

2026/7/19 14:49:04阅读更多 →
HarmonyOS:ArkTS数据模型的设计与演进实战

HarmonyOS:ArkTS数据模型的设计与演进实战

引子:数据设计好了,UI 就是套模板 星空运势应用经历了两次数据结构的变化。第一版是四个文件——Types.ts、SignsData.ts、TarotData.ts、Constants.ts,每个文件职责单一。重构后合并成了一个 DataModel.ets,12 星座的数据从 5 个…

2026/7/19 14:49:04阅读更多 →
视频分析终极指南:如何用AI让机器看懂视频内容

视频分析终极指南:如何用AI让机器看懂视频内容

视频分析终极指南:如何用AI让机器看懂视频内容 【免费下载链接】video-analyzer Analyze videos using LLMs, Computer Vision and Automatic Speech Recognition 项目地址: https://gitcode.com/gh_mirrors/vi/video-analyzer 你是否曾经面对海量视频内容感…

2026/7/19 22:46:53阅读更多 →
LangGraph 是什么?为什么 Agent 需要图结构

LangGraph 是什么?为什么 Agent 需要图结构

前面 RAG 系列里,我们一直在讨论一个问题: AI 应用不只是把问题发给大模型,然后把答案返回给用户。 真正进入业务以后,系统往往需要处理文档检索、工具调用、权限判断、多轮对话、失败重试、人工确认和日志追踪。 这些能力叠在一起…

2026/7/19 22:46:53阅读更多 →
Cursor vs Windsurf 2026:谁才是AI编程之王?

Cursor vs Windsurf 2026:谁才是AI编程之王?

Cursor vs Windsurf 2026深度对比:哪个AI编程助手更适合你? > 一句话结论: Cursor 在代码补全和 Agent 模式上更成熟,Windsurf 在上下文感知和 IDE 集成上更激进。两者都远远甩开了传统编辑器Copilot的组合。至于选哪个——看…

2026/7/19 22:46:53阅读更多 →
synchronized 锁释放时的“竞争“

synchronized 锁释放时的“竞争“

一、先说结论:这里的"竞争"是指多个线程同时试图获取同一把锁,但锁只有一个,最终只有一个能成功,其他失败者必须等待或重试。 你可以把锁想象成一个只能容纳1个人的卫生间,线程就是想上厕所的人。 竞争:多个人同时冲向卫生间,去拉门把手(尝试获取锁)。 抢到…

2026/7/19 22:46:53阅读更多 →
AQS 锁导致cpu飙高原因和synchronized 是一样的吗

AQS 锁导致cpu飙高原因和synchronized 是一样的吗

不完全一样,但本质都是“无效计算”消耗CPU,只是各自的“无效计算”形式不同。 为了让你彻底看透,我从底层机制和CPU消耗路径两个维度来拆解。 一、先看结论:AQS 和 synchronized 的“耗电”路径不同 对比维度 AQS锁(如ReentrantLock) synchronized(重量级锁) CPU消耗…

2026/7/19 22:46:53阅读更多 →
【DWT】计算两不等序列相似度:DWT

【DWT】计算两不等序列相似度:DWT

note DTW(DP-matching)用于计算两个不等长、但变化趋势相关的序列的相似度。论文给出了最优的对齐规则:对称权重 斜率约束P1,让对齐更准,识别错误率降到了原来的2/3针对两个时长不同、采样点数量不一致的语音特征序列…

2026/7/19 22:44:53阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/19 0:01:04阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/19 0:01:04阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/19 0:01:04阅读更多 →
Go语言静态资源打包方案对比与实践指南

Go语言静态资源打包方案对比与实践指南

1. 项目背景与核心需求在Go语言开发中,我们经常需要处理静态资源文件的打包问题。无论是Web应用的模板文件、前端资源,还是配置文件、证书等,都需要随程序一起分发。传统做法是将这些文件与编译后的二进制文件放在同一目录下,但这…

2026/7/19 0:01:04阅读更多 →
Go语言实现高性能LDAP认证服务的架构与实践

Go语言实现高性能LDAP认证服务的架构与实践

1. 项目背景与核心价值LDAP(轻量级目录访问协议)作为企业级身份认证的黄金标准,已经服务了超过80%的财富500强公司。我在金融科技领域实施统一认证体系时,发现传统Java方案存在启动慢、内存占用高等痛点。而Go语言凭借其协程并发模…

2026/7/19 0:01:04阅读更多 →
【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

【AI面试官实战指南】:用ChatGPT模拟10类高频技术岗面试,3天提升应答精准度92%

更多请点击: https://intelliparadigm.com 第一章:AI面试官实战指南的核心价值与适用场景 AI面试官并非替代人类HR的“黑箱工具”,而是以可解释、可审计、可迭代的方式,赋能招聘全链路的关键基础设施。其核心价值在于将主观经验沉…

2026/7/19 0:01:04阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/18 22:49:46阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/19 14:50:26阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/19 18:50:36阅读更多 →