摘要本文详细介绍如何通过 Vue 3 Element Plus 实现 AI 聊天接口的流式调用即“逐字输出”的打字机效果。从最基础的正常调用一次性返回出发逐步讲解流式输出的核心原理ReadableStreamfetch分块读取并给出完整代码。随后针对实际使用中的体验痛点提供了自动滚动到底部的优化方案判断内容溢出与用户滚动行为最后总结了一系列其他优化建议URL 编码、并发锁、取消请求、错误处理等。适合正在开发聊天机器人前端的同学参考。一、背景AI 对话的两种调用方式当我们调用后端的 AI 聊天接口如 GPT 类模型时常见的返回方式有两种方式行为体验正常调用一次性输出后端完整生成答案后一次性返回用户需等待数秒甚至十多秒期间界面空白或转圈容易焦虑流式调用逐字输出后端每生成一个词或一小段就立即发送前端收到后实时显示文字逐字出现像打字机反馈即时用户体验好显然流式调用更适合对话场景。下面我们先看一个最简单的流式调用实现然后分析其原理最后做各种优化。二、最简单的流式调用实现以下是一个基于 Vue 3 Element Plus 的流式调用示例仅核心功能vuetemplate el-input v-modelquestion placeholder请输入问题 / el-button typeprimary clickask发送/el-button el-input v-modelanswer typetextarea :autosize{ minRows: 3, maxRows: 10 } placeholder回答 / /template script setup import { ref } from vue const question ref() const answer ref() const memoryId ref(1) const ask async () { if (!question.value.trim()) return answer.value const url http://localhost:9000/api/chat/chat01?memoryId${memoryId.value}message${question.value} try { const response await fetch(url, { method: GET, headers: { Accept: text/html;charsetutf-8 } }) if (!response.ok) throw new Error(请求失败) const reader response.body.getReader() const decoder new TextDecoder(utf-8) while (true) { const { done, value } await reader.read() if (done) break const chunk decoder.decode(value, { stream: true }) answer.value chunk } } catch (error) { answer.value 出错了请稍后重试 } } /script原理大白话解读后端配合后端接口支持分块传输Transfer-Encoding: chunked 或 SSE生成一点就发一点。前端逐块读取fetch得到的response.body是一个ReadableStream可读流。调用getReader()拿到读取器然后用while循环不断调用reader.read()。每次取到一小块二进制数据value用TextDecoder解码成字符串拼接到answer.value后面。实时渲染由于 Vue 是响应式的每次拼接都会触发 DOM 更新用户就看到文字一个一个蹦出来了。三、痛点内容超出可视区后需手动滚动上面的代码虽然实现了流式输出但有一个明显的体验问题当回答内容超过文本框的可视高度时新出现的文字不会自动滚动到底部用户必须手动拖动滚动条。期望的行为是内容未超出可视区 → 无需滚动内容超出可视区且用户当前没有主动向上翻阅历史 → 自动滚动到底部让用户始终看到最新的内容如果用户向上滚动查看旧内容 → 停止自动滚动尊重用户的阅读位置四、自动滚动的实现方案4.1 关键步骤给el-input添加ref以便获取内部的textareaDOM 元素。监听textarea的scroll事件判断用户是否在底部附近。使用watch监听answer的变化在每次新增内容后判断内容是否溢出scrollHeight clientHeight判断用户是否处于底部滚动条距离底部小于 10px两个条件都满足时才将scrollTop设置为scrollHeight。4.2 代码修改在原有基础上添加修改模板部分vueel-input refanswerTextareaRef v-modelanswer typetextarea :autosize{ minRows: 3, maxRows: 10 } placeholder回答 scrollonScroll /修改脚本部分jsimport { ref, nextTick, watch } from vue // ... 原有代码 const answerTextareaRef ref(null) let isUserScrollingUp false // 标记用户是否主动向上滚动 // 监听 textarea 滚动事件 const onScroll () { const textareaEl answerTextareaRef.value?.$el?.querySelector(textarea) if (!textareaEl) return // 距离底部小于 10px 认为在底部 const isAtBottom (textareaEl.scrollHeight - textareaEl.scrollTop - textareaEl.clientHeight) 10 isUserScrollingUp !isAtBottom } // 自动滚动到底部条件满足时才滚动 const autoScrollToBottom async () { await nextTick() // 等待 DOM 更新完成 const textareaEl answerTextareaRef.value?.$el?.querySelector(textarea) if (!textareaEl) return const isOverflow textareaEl.scrollHeight textareaEl.clientHeight // 是否溢出 if (isOverflow !isUserScrollingUp) { textareaEl.scrollTop textareaEl.scrollHeight } } // 监听 answer 变化触发自动滚动 watch(answer, () { autoScrollToBottom() }) // 在 ask 函数开头重置滚动标记新问题默认用户在底部 const ask async () { isUserScrollingUp false // 新增 // ... 原有代码 }4.3 为什么这样写nextTick确保 Vue 已经把最新的answer渲染到textarea中否则scrollHeight可能还是旧值。判断溢出没有滚动条时滚动也没意义。尊重用户行为用户向上滚动时不再强制拉回底部只有当他主动滚回底部或发送新问题时才恢复自动滚动。五、更多优化建议提升健壮性与体验除了自动滚动实际项目中还建议做以下优化按优先级排序 高优先级优化点原因修复 URL 参数未编码使用encodeURIComponent(question.value)消息中的、#、中文等会破坏 URL 结构添加并发锁用isLoading标志防止重复点击避免同时发起多个请求界面错乱 中优先级优化点原因支持取消请求使用AbortController 停止按钮用户可主动中断长时间生成节省资源响应式布局放弃硬编码margin-left改用 Flex/Grid适配不同屏幕尺寸细化错误处理区分网络错误、超时、用户取消等给出明确提示便于用户操作超时控制比如 30 秒无响应则中断并提示避免永久等待 低优先级增强功能优化点原因Markdown 渲染用marked库将回答转为 HTMLAI 常返回代码块、列表纯文本难阅读复制回答按钮方便用户保存内容多轮对话界面用消息数组渲染气泡更像真实聊天记录本地持久化会话 ID用localStorage存储memoryId不同会话互不干扰六、完整示例含自动滚动 取消请求 并发锁vuetemplate div classchat-container div classinput-area el-input v-modelquestion placeholder请输入问题 keyup.enterask / el-button typeprimary clickask :loadingisLoading发送/el-button el-button v-ifisLoading clickstopGeneration typewarning停止/el-button /div el-input refanswerTextareaRef v-modelanswer typetextarea :autosize{ minRows: 5, maxRows: 15 } readonly scrollonScroll / /div /template script setup import { ref, nextTick, watch } from vue const question ref() const answer ref() const memoryId ref(1) const isLoading ref(false) let abortController null const answerTextareaRef ref(null) let isUserScrollingUp false const onScroll () { const textareaEl answerTextareaRef.value?.$el?.querySelector(textarea) if (!textareaEl) return const isAtBottom (textareaEl.scrollHeight - textareaEl.scrollTop - textareaEl.clientHeight) 10 isUserScrollingUp !isAtBottom } const autoScrollToBottom async () { await nextTick() const textareaEl answerTextareaRef.value?.$el?.querySelector(textarea) if (!textareaEl) return const isOverflow textareaEl.scrollHeight textareaEl.clientHeight if (isOverflow !isUserScrollingUp) { textareaEl.scrollTop textareaEl.scrollHeight } } watch(answer, () { autoScrollToBottom() }) const ask async () { if (!question.value.trim() || isLoading.value) return isLoading.value true isUserScrollingUp false answer.value if (abortController) abortController.abort() abortController new AbortController() const url http://localhost:9000/api/chat/chat01?memoryId${memoryId.value}message${encodeURIComponent(question.value)} try { const response await fetch(url, { signal: abortController.signal, headers: { Accept: text/html;charsetutf-8 } }) if (!response.ok) throw new Error(请求失败) const reader response.body.getReader() const decoder new TextDecoder(utf-8) while (true) { const { done, value } await reader.read() if (done) break const chunk decoder.decode(value, { stream: true }) answer.value chunk } } catch (error) { if (error.name AbortError) { answer.value 已停止生成 } else { console.error(error) answer.value 出错了请稍后重试 } } finally { isLoading.value false abortController null } } const stopGeneration () { if (abortController) { abortController.abort() abortController null isLoading.value false } } /script style scoped .chat-container { max-width: 900px; margin: 20px auto; padding: 0 20px; } .input-area { display: flex; gap: 12px; margin-bottom: 20px; } .el-input { flex: 1; } /style七、总结流式调用是提升 AI 对话体验的关键技术其核心在于前端通过ReadableStream分块读取后端实时生成的数据。然而仅有流式还不够自动滚动、取消请求、错误处理等细节决定了产品的完成度。本文提供的自动滚动方案充分考虑了用户行为向上翻阅时不打扰可直接应用到生产项目中。其他优化建议也可按需逐步实现。希望这篇文章能帮助你打造一个流畅、友好的聊天机器人界面。如果你有更好的建议或疑问欢迎在评论区交流喜欢本文的话别忘了点赞、收藏、关注你的支持是我更新的动力~