效果为什么用长连接实时交互需求用户与大模型交互时希望能实时看到回答的生成过程长连接可让后端将生成的内容逐段实时传给前端实现类似 “边生成边显示” 的效果提升交互体验。应对大数据量传输大模型回答可能包含大量数据长连接能在一次连接中持续传输避免短连接因频繁建立和断开导致的性能损耗保证数据传输的稳定性和完整性。这里使用的是通过fetch结合ReadableStream实现长连接不是websocket区别如下1. 协议层面fetchReadableStream本质上基于 HTTP 协议。HTTP 是一种无状态的协议虽然可以通过流式传输数据来模拟长连接但它仍然是基于请求 - 响应模型客户端发起请求服务器返回响应只是响应数据可以分块逐步传输。WebSocket基于 WebSocket 协议它是一种独立的、双向的网络协议在 TCP 协议之上运行。WebSocket 连接一旦建立就可以在客户端和服务器之间进行全双工通信即双方可以同时发送和接收数据无需像 HTTP 那样每次通信都要发起新的请求。2. 连接建立过程fetchReadableStream客户端通过fetchAPI 发起一个 HTTP POST 请求服务器接收到请求后开始返回响应数据。如果服务器支持流式传输就会持续不断地发送数据块客户端通过ReadableStream逐步读取这些数据。WebSocket客户端通过WebSocket构造函数创建一个 WebSocket 实例向服务器发起一个特殊的 HTTP 升级请求HTTP Upgrade request请求将连接从 HTTP 协议升级到 WebSocket 协议。如果服务器同意升级双方就会建立一个 WebSocket 连接。3. 数据传输方式fetchReadableStream数据传输是单向的通常是客户端发起请求服务器返回数据。数据以文本形式传输通常使用特定的格式如 Server-Sent Events 格式来区分不同的事件和数据块。WebSocket支持双向数据传输客户端和服务器可以随时向对方发送数据。数据可以是文本或二进制格式开发者可以根据需要灵活选择。4. 实时性和性能fetchReadableStream由于基于 HTTP 协议存在一定的延迟特别是在处理大量数据或频繁交互时。每次请求都需要携带 HTTP 头部信息增加了数据传输的开销。WebSocket实时性更好因为它是全双工通信没有请求 - 响应的延迟。而且 WebSocket 连接建立后后续的数据传输只需要携带少量的协议头部信息减少了数据传输的开销性能更高。5. 错误处理和重连机制fetchReadableStream错误处理相对复杂需要处理 HTTP 状态码、网络错误等多种情况。重连机制需要开发者手动实现当连接中断时需要重新发起fetch请求。WebSocket提供了内置的错误处理和重连机制。当连接出现错误或断开时会触发相应的事件如onerror、onclose开发者可以监听这些事件并实现重连逻辑。6. 适用场景fetchReadableStream适用于一次性请求需要逐步接收大量数据的场景如文件下载、服务器推送实时日志等。WebSocket适用于需要实时双向通信的场景如聊天应用、实时游戏、实时数据监控等。如何实现1. 数据接收与预处理在handleEvent方法中当接收到answer事件时对事件数据进行预处理。使用正则表达式替换特殊字符将\\n替换为\n并把连续的换行符统一为\n\n。2. 内容更新与缓冲将处理后的数据添加到completeMarkdown中该变量用于存储完整的 Markdown 内容。同时将处理后的数据添加到answerBuffer中answerBuffer作为一个缓冲区暂存待渲染的新内容。3.防抖渲染机制为避免频繁渲染影响性能使用防抖技术。每次接收到新数据时清除之前设置的定时器this.renderTimer并重新设置一个 100ms 的定时器。如果在 100ms 内没有新数据到达则调用renderMarkdown方法进行渲染。但是随着时间间隔增大生成答案的效果会变得更卡顿在意生成流畅度可以不适用定时器改用其他性能优化手段4. 强制渲染条件通过shouldForceRender方法判断当前接收到的数据是否包含特定的强制渲染标记如###、##、\n\n、。、、。如果包含这些标记则立即调用renderMarkdown方法进行渲染确保在关键位置及时更新页面。5. 渲染与更新 DOM在renderMarkdown方法中使用this.md.render方法将完整的 Markdown 内容渲染为 HTML。使用this.$nextTick确保在 DOM 更新后执行后续操作。更新answerMarkdown变量将渲染后的 HTML 内容赋值给它从而更新页面显示。清空answerBuffer缓冲区为下一次渲染做准备。调用scrollToBottom方法将页面滚动到底部确保用户能看到最新的内容。示例div classcontentText v-ifchat.type ! user div classthinking v-htmlthinkingMarkdown /div div v-htmlanswerMarkdown/div /div export default { data() { return { thinkingMarkdown: , answerMarkdown: , answerBuffer: , completeMarkdown: , // 存储完整的 markdown renderTimer: null, md: null } }, methods: { handleEvent(event) { if (event.name answer) { // 1. 处理数据 const normalizedData event.data .replace(/\\n/g, \n) .replace(/\r?\n\r?\n/g, \n\n); // 2. 更新完整内容 this.completeMarkdown normalizedData; // 3. 添加到缓冲区 this.answerBuffer normalizedData; // 4. 使用防抖进行渲染 clearTimeout(this.renderTimer); this.renderTimer setTimeout(() { this.renderMarkdown(); }, 100); // 100ms 的防抖时间 // 5. 强制在特定标记处渲染 if (this.shouldForceRender(normalizedData)) { this.renderMarkdown(); } } }, shouldForceRender(text) { // 定义需要强制渲染的标记 const forceRenderMarkers [ ###, ##, \n\n, 。, , ]; return forceRenderMarkers.some(marker text.includes(marker)); }, renderMarkdown() { if (!this.answerBuffer) return; try { // 渲染新内容 const renderedContent this.md.render(this.completeMarkdown); this.$nextTick(() { // 更新 DOM this.answerMarkdown renderedContent; // 清空缓冲区 this.answerBuffer ; // 滚动到底部如果需要 this.scrollToBottom(); }); } catch (error) { console.error(Markdown 渲染错误:, error); } }, scrollToBottom() { const element this.$el.querySelector(.left); if (element) { element.scrollTop element.scrollHeight; } } }, // 组件销毁时清理 beforeDestroy() { if (this.renderTimer) { clearTimeout(this.renderTimer); } } }6.总结接收事件数据对其预处理替换特殊字符。将处理后的数据添加到完整内容存储处和缓冲区。用防抖机制若 100ms 无新数据则渲染含特定标记时强制渲染。渲染 Markdown 为 HTML更新 DOM 并清空缓冲区滚动页面到底部。组件销毁时清除定时器防内存泄漏。