前端打字机效果:流式输出从0到1手把手教学
摘要本文介绍了通过前端调用 AI 聊天接口的两种方式正常调用一次性返回完整回答与流式调用逐字输出。首先展示了在浏览器地址栏直接测试接口的效果。然后分别给出基于 Vue 3 Element Plus 的代码实现正常调用通过封装的chat接口一次性获取答案流式调用则使用fetchReadableStream逐块读取后端返回的数据并实时拼接到页面实现“打字机”效果。文章总结了流式调用的核心原理——后端分块生成、前端循环读取并累加显示。最后对比了两者的优缺点并提供了后续优化建议涵盖界面布局、并发控制、错误处理、请求取消、Markdown 渲染等方向为开发者构建更流畅的聊天交互提供了实用参考。目录一. 目前已有的东西二.通过前端调用该接口1.正常调用优点缺点2.流式调用优点缺点后续优化建议一. 目前已有的东西下面我们直接在浏览器的地址栏调用一下这个接口效果如下二.通过前端调用该接口1.正常调用template el-input v-modelquestion stylewidth: 200px;margin-left:700px placeholder请输入问题 / el-button typeprimary clickask发送/el-button br el-input v-modelanswer typetextarea :autosize{ minRows: 3, maxRows: 10 } stylewidth: 800px;margin-left:450px placeholder回答 / /template script setup import { ref } from vue import { chat } from /api/chatApi; //变量1用户的提问 const question ref() //变量2后端接口返回的答案 const answer ref() //变量3入参 const dto ref( { memoryId:1, message: } ) //方法1调用后端接口获得回答 const ask async(){ dto.value.message question.value; const resp await chat(dto.value); answer.value resp; } /script style scoped /* 这里可以添加组件专用的样式 */ /* scoped 属性确保样式只作用于当前组件 */ /style效果测试优点实现极其简单– 后端只要正常返回完整 JSON 或文本前端await response.json()或response.text()一行代码就能拿到全部数据。调试方便– 抓包看到一个完整响应很直观不需要处理碎片拼接。数据完整性好– 不会出现半个字符、断包的问题天然就是完整的。适用于短小数据– 如果结果长度只有几个字符一次性返回反而更干净。缺点用户等待时间长– 比如 AI 生成 1000 字需要 10 秒用户只能盯着空白屏幕或转圈圈容易烦躁甚至关掉页面。内存占用高– 后端要先把 1000 字全部存到内存再一次性发送前端也要等全部收到才能显示大响应会消耗更多内存。无法提前反馈– 即使用户看到前半段已经觉得不对也不能取消因为后端还在默默生成后半段。超时风险– 如果生成时间过长网关或浏览器可能触发超时断开连接导致全部白费。2.流式调用template el-input v-modelquestion stylewidth: 200px;margin-left:700px placeholder请输入问题 / el-button typeprimary clickask发送/el-button br el-input v-modelanswer typetextarea :autosize{ minRows: 3, maxRows: 10 } stylewidth: 800px;margin-left:450px placeholder回答 / /template script setup import { ref } from vue const question ref() const answer ref() const memoryId ref(1) // 你的记忆ID //点击发送按钮时执行的函数 const ask async () { //如果问题为空或者只有空格就不发送 if (!question.value.trim()) return //清空上一次的回答 answer.value //构建请求 URL注意你的后端路径是 /api/chat/chat01 const url http://localhost:9000/api/chat/chat01?memoryId${memoryId.value}message${question.value} try { //使用 fetch 发送 GET 请求注意后端返回的是流stream const response await fetch(url, { method: GET, headers: { Accept: text/html;charsetutf-8// 告诉后端我接受UTF-8文本 } }) //如果HTTP状态码不是200之类就报错 if (!response.ok) throw new Error(请求失败) // ★★★ 流式输出的核心原理开始记这一段代码逻辑即可 ★★★ // response.body 是一个 ReadableStream可读流类似一个水管 // getReader() 拿到这个水管的“读取器”可以一点一点从水管里取水数据 const reader response.body.getReader() // 创建文本解码器把后端传过来的二进制数据比如UTF-8编码的字节转成字符串 const decoder new TextDecoder(utf-8) // 无限循环直到所有数据都被读完 while (true) { // 从流中读取一块数据done表示是否已经读完value是这一块的二进制数据Uint8Array const { done, value } await reader.read() // 如果后端传完了所有数据就退出循环 if (done) break // 把这一小块二进制数据解码成字符串stream: true 表示可能还没结束中间可能会有不完整的字符解码器会处理 const chunk decoder.decode(value, { stream: true }) // 把这一小块字符串拼接到已有的 answer 后面 —— 这就是“流式输出”的效果 // 后端每吐一个字前端的文本框就多一个字看起来就像打字机一样 answer.value chunk } } catch (error) { console.error(error) answer.value 出错了请稍后重试 } } /script style scoped /* 这里可以添加组件专用的样式 */ /* scoped 属性确保样式只作用于当前组件 */ /style效果测试原理总结1. 后端不是一次性把全部回答算完再返回而是生成一点就发送一点比如每生成一个词就发一次2. 前端 fetch 接收到这种分块传输的数据通过 reader.read() 一次次拿取每个数据块3. 每次拿到一个小块就立刻显示到页面上不断累加用户就看到文字逐渐出现4. 如果后端是 AI 模型比如 ChatGPT通常就是用这种方式实现“逐字输出”优点用户体验好– 不用干等十几秒能看到文字“一个字一个字往外蹦”感觉系统在实时思考心里有底。减少焦虑– 如果内容很长一次性等太久用户可能以为程序卡死了流式输出能让用户知道“还在工作中”。可中途取消– 如果发现回答不对用户可以提前中断请求节省带宽和计算资源。内存占用低– 前端不用等整个巨大响应全部到齐再渲染后端的生成结果也可以边产生边发送不占用大量内存。缺点实现复杂– 后端需要支持分块传输比如 Server-Sent Events 或直接写流前端要用ReadableStream反复读取还要处理断网、错误重试等。网络开销稍大– 每个小块都有一些额外的 HTTP 分块头部总数据量会比一次性输出略大一点点但通常可忽略。不适合所有场景– 如果数据本来就很短比如几十个字流式输出的优势不明显反而增加复杂度。后续优化建议①界面设置优化放弃硬编码margin-left改用 Flex/Grid 实现响应式居中并限制容器最大宽度如 900px原因固定像素偏移在不同屏幕尺寸下会错位Flex/Grid 能自适应各种设备提升界面兼容性增加“停止生成”按钮配合AbortController让用户能主动中断流式输出原因流式输出可能耗时较长用户若发现回答偏离预期或等待不耐烦应能主动取消避免资源浪费和不良体验发送后清空输入框并在请求期间禁用发送按钮避免并发重复请求原因连续点击会同时发起多个请求造成前端显示错乱、后端压力增大禁用按钮是标准的交互保护当回答内容超过可视区域时自动滚动到最新输出位置使用nextTick操作滚动条原因流式输出时用户通常关注最新出现的文字自动滚动可省去手动下拉的麻烦符合阅读习惯增加“复制回答”按钮方便用户一键获取内容原因用户常需要将 AI 的回答粘贴到其他地方提供复制按钮能减少选中和拷贝的操作成本②代码逻辑优化修复 URL 参数未编码的严重 bug必须用encodeURIComponent(question.value)包裹消息内容原因用户消息中可能包含、#、空格、中文等特殊字符直接拼接到 URL 会破坏参数结构导致后端接收到错误的消息encodeURIComponent可将这些字符转为安全的百分号编码添加isLoading锁标志防止用户在请求进行中再次点击发送原因未加锁时用户多次点击会创建多个并发请求后面返回的数据会互相覆盖显示区域且后端同一 memoryId 可能被打乱上下文引入AbortController实现真正的请求取消并在组件卸载onUnmounted时自动 abort原因用户切换页面或关闭组件时应立刻断开网络连接避免已卸载的组件尝试更新状态导致内存泄漏或控制台报错增加超时控制如 30 秒避免因后端无响应而永久等待原因网络抖动或后端服务故障可能导致 fetch 永不完成用户界面会一直处于加载中状态超时后可给出明确提示并重置 UI细化错误处理区分网络错误、HTTP 错误、用户主动取消、超时等场景给出不同提示原因不同的错误原因需要不同的用户反馈例如“网络断开” vs “已停止生成”笼统的“出错了”不利于用户判断下一步操作流式数据格式适配如果后端返回的不是纯文本如 SSE 格式的data: {...}需要按约定解析再拼接原因后端可能返回标准 SSE 事件流或 JSON 分块直接拼接原始字符串会导致显示多余格式字符需要前端根据协议提取实际文本内容③功能扩展建议支持多轮对话将消息存储为数组messages渲染对话气泡并利用memoryId维持后端上下文原因单 answer 字段每次提问会覆盖上一轮回答无法形成对话历史数组结构可完整展示用户与 AI 的交流过程对回答内容进行 Markdown 渲染如使用marked库提升代码块、列表等内容的可读性注意 XSS 防护原因AI 生成的回答常含 Markdown 语法纯文本显示会暴露原始标记符号渲染后更符合阅读习惯且支持语法高亮本地持久化记忆 ID用localStorage生成或获取唯一sessionId允许用户手动“新建会话”原因固定 memoryId 会导致所有用户和所有对话共用同一个上下文互相干扰为每个会话分配独立 ID 可隔离不同对话加入自动重试机制网络临时故障时自动重试 1~2 次提升稳定性原因短暂的网络波动或后端重启可能导致第一个数据块失败自动重试能在不打扰用户的情况下恢复请求提高成功率④优先级参考 高优先级URL 编码、并发锁防止重复请求原因这两项直接影响功能的正确性和稳定性不修复会导致请求错乱或界面卡死 中优先级取消请求、响应式布局、加载状态、错误细化原因显著提升用户体验和代码健壮性但不是最紧急的致命问题 低优先级Markdown 渲染、复制按钮、多轮对话界面、重试机制原因属于增强型功能可在核心功能稳定后逐步添加以上就是本篇文章的全部内容喜欢的话可以留个免费的关注呦~~~