简介面向 Vue 开发者的 AI 聊天页面项目基于 Vue 3 与组合式 API 搭建对话式 UI解决聊天界面从无到有、组件状态管理与前后端交互等常见问题。压缩包内共 79 个文件整体约 668KB主要包含 Vue 组件、JavaScript 脚本、路由与状态管理配置、构建文件及页面静态资源还附带了完整 Git 历史与示例工程结构可快速运行、修改和二次开发。目前已有 1110 人学习下载适合具备 Vue 基础、希望获取完整聊天页面代码和工程组织逻辑的初学者。项目中的目录划分与配置示例能帮助读者理解组件拆分、接口调用和状态同步的关键思路有效缩短原型开发周期。1. 把AI聊天页面做出来远不止“调个API”那么简单最近我一直在折腾一个基于Vue 3的AI聊天页面从最初的“给大模型套个壳”到后来把对话体验、流式渲染、会话管理全部做扎实中间踩了不少坑也沉淀了不少可复用的经验。老实说现在网上的AI应用越来越多聊天页面几乎成了AI落地的“门面担当”但真正能把体验做得顺滑、代码结构做得清晰的项目并不多。这篇内容不是泛泛的“Vue 3入门教程”而是一个围绕“AI聊天页面”这个具体场景的完整实践复盘。我会把从技术选型、消息数据结构设计、流式输出实现、Markdown渲染、异常处理到性能优化的全过程都拆开讲项目里用到的代码片段可以直接抄走改改用。不管你是刚接触Vue 3不久的前端新手还是已经写过不少业务页面、想切入AI应用开发的同学这篇文章应该都能给你一些参考。先说说我最终做出来的效果左侧是会话列表可以新建多个对话右侧是聊天主区域支持用户发送消息、AI流式打字输出、代码高亮、Markdown渲染、复制消息、停止生成、重试对话整套交互基本对标Claude或ChatGPT的网页版体验。核心的AI能力通过后端转发的大模型API实现前端只负责对话状态管理、流式数据解析和界面渲染这种架构也是目前最主流、最适合中小团队快速落地的方式。2. 整体设计方案与技术选型2.1 为什么选择Vue 3来做AI聊天页面选Vue 3不是因为它最潮而是它在“聊天类应用”这个具体场景下有几个实打实的优势。第一是组合式APIComposition API对逻辑复用的支持非常好聊天页面的核心逻辑可以拆成useChat、useStream、useMarkdown这类独立的组合函数各个模块之间互不干扰维护起来轻松得多。第二是响应式系统对高频状态更新的处理足够高效聊天过程中消息列表、输入状态、加载状态都在不断变化Vue 3基于Proxy的响应式机制在性能上比Vue 2提升了不少。第三点可能更实在——Vue 3的生态已经非常成熟了。Vite作为构建工具冷启动和热更新快到离谱Pinia作为状态管理库写法简洁且天然适配TypeScript再配合ESLint和Prettier整个项目的开发体验在新一代前端框架里绝对属于第一梯队。对于AI应用来说开发速度就是竞争力Vue 3这套组合拳能让你把精力集中到对话逻辑本身而不是和构建工具较劲。2.2 项目整体架构前端负责体验后端负责安全AI聊天页面的架构设计有个关键点一定要想清楚API Key绝对不能直接放在前端代码里。浏览器环境下所有代码都是暴露的把Key写死在环境变量里一样能被有心人挖出来轻则被刷爆账单重则引发安全问题。所以我采用了一个简单的代理方案后端用Node.js或Python写一个轻量转发接口前端把用户消息发给自己的后端后端再携带真实的API Key去请求大模型服务拿到数据流后原样转发给前端。这样做的好处至少有两点第一敏感凭证只存在于后端环境变量中安全风险大幅下降。第二后端可以在转发层做额外的业务逻辑比如用户鉴权、消息敏感词过滤、请求频率限制、数据落库等这些逻辑如果全部堆在客户端既不安全又不好维护。前端部分我用Vite搭建了一个Vue 3 TypeScript的项目骨架UI框架选了Element Plus状态管理用了Pinia网络请求直接使用浏览器原生的fetch API没有额外引入Axios——因为流式读取请求用fetch的response.body.getReader()来处理最为直接Axios对流式响应和中断取消的支持反而没有原生fetch来得顺手。2.3 消息数据结构设计一张表说清楚聊天页面的核心数据模型就是“会话”和“消息”。我在Pinia的store里设计了这样一组类型export interface ChatMessage { id: string; role: user | assistant; content: string; createdAt: number; status?: pending | streaming | done | error; model?: string; } export interface ChatSession { id: string; title: string; messages: ChatMessage[]; createdAt: number; updatedAt: number; }这里把status字段单独拎出来说明一下它在流式输出中承担了非常重要的角色用户发送消息后AI消息的初始状态是streaming每收到一个数据块就追加到content后面流结束后状态切换为done请求出错则切为error同时保留已输出的部分内容方便用户判断“断在哪里了”。有了这个状态机前端就可以渲染出不同的UI样式——比如输入中显示转圈动画流式阶段显示闪烁光标出错时显示重试按钮整个交互会清晰很多。3. 核心功能模块解析3.1 流式输出AI聊天页面的灵魂流式输出是AI聊天页面和普通聊天软件最核心的区别。用户的提问经过大模型推理通常需要几秒甚至几十秒才能完整生成如果等全部生成完再一次性显示体验会非常糟糕。而流式输出可以让文本像打字机一样逐字蹦出来用户反馈速度“快多了”实际上网络传输总量并没有变少变少的是用户感知上的等待时间。实现流式输出的核心是后端API的SSEServer-Sent Events或纯流式响应模式。目前绝大多数兼容OpenAI格式的推理服务都支持stream: true参数服务端会不断返回data: {json chunk}格式的内容每个chunk里包含一小段增量文本。在前端我用fetch发送POST请求并设置stream: true然后通过ReadableStream来逐块读取const response await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ messages, stream: true }), }); 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 }); // 解析chunk中的data字段提取增量文本 handleStreamChunk(chunk); }这里有个细节值得注意TextDecoder的第二个参数传了{ stream: true }这个设置可以处理多字节字符被拆散到不同chunk的情况。流式传输下一个中文字符的UTF-8编码可能被切分到两个chunk里如果不做流式解码就会出现中文乱码。我一开始没加这个参数结果AI回答里的中文经常出现“”这种乱码字符排查了半天才发现是解码方式的问题。3.2 流式数据的解析与格式兼容解析流式数据时要处理的不仅仅是data: {json}这种标准格式。我实测下来不同服务商在流式输出的边界处理上差异很大有的在末尾发送data: [DONE]有的不发有的每个chunk都包含完整的JSON对象有的一个chunk里塞了多行data还有的在文本内容中直接嵌入换行符容易导致按行解析时把一行JSON“劈”成两半。为了兼容这些情况我写了一个轻量的解析器先把收到的字符串按换行符拆成数组再逐行判断是否以data:开头。如果某个data行不是合法JSON就先存到缓冲区里等下一轮数据到达时拼接再解析。这个方案虽然不复杂但在实际使用中帮我省去了大量兼容不同模型服务的麻烦。另外一个关键点是增量内容字段名。OpenAI兼容格式里chunk中的增量文本在choices[0].delta.content但有些服务用的是choices[0].message.content还有的干脆把整段文本放在choices[0].text里。为了同时兼容我在解析时做了多重兜底只要命中任一字段就取出来追加。这也是AI聊天页面开发中非常常见的一个坑——你以为你对接的是“标准协议”实际上每家的“标准”都有点自己的脾气。3.3 Markdown渲染与代码高亮别让AI的回答变成“白底黑字”流式输出解决之后下一个问题就是渲染。大模型的回答几乎都是Markdown格式的包含标题、列表、表格、代码块、行内代码等。直接当成纯文本渲染读者看代码块时简直是一场灾难。但如果用常规Markdown库渲染又会遇到流式输出场景下的特殊问题用户在AI还没回答完时看到的是一段“不完整”的Markdown中间态比如一个还没闭合的三反引号代码块、一个未完成的表格这时候如果实时解析并渲染页面会出现闪烁或排版跳动。我的方案是Markdown解析渲染的频率不需要实时逐字执行而是通过节流的方式每60到100毫秒重新渲染一次当前的消息内容。这样既保证了代码块和格式能最终正确展示又不会因为每个字都触发一次渲染导致性能下降。渲染库我选的是marked配合highlight.js轻量且生态成熟import { marked } from marked; import hljs from highlight.js; marked.setOptions({ highlight(code, lang) { if (lang hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value; } return hljs.highlightAuto(code).value; } }); // 渲染时注意让组件只处理增量内容 const html marked.parse(content);关于代码高亮我再提醒一句marked客户端渲染存在一个XSS风险因为AI生成的内容里可能包含危险的HTML标签或javascript:链接。常规做法是引入DOMPurify对渲染后的HTML做一次清洗。我在项目里把清洗逻辑放在渲染管线的最后一步确保所有外部内容都经过消毒后再进入页面。这一步绝不能省尤其是如果你未来打算做“AI聊天网页版”直接公网部署安全上线是底线。3.4 会话管理与页面交互细节聊天气泡的发送、停止、重试、复制这些基础交互看起来简单真要打磨到顺手级别还是有不少细节。我会话列表用localStorage做了持久化刷新页面不会丢失历史对话。每次用户发送消息时系统会把最近一轮消息写入updatedAt会话自动排序到列表最上方。新建会话时自动生成一个包含日期时间的默认标题用户也可以稍后重命名。交互上我额外做了三个功能发送消息时如果AI正在回复同时用户再次点击发送会采用“打断旧请求再发新消息”的策略AI回复过程中输入框旁边会出现一个“停止生成”按钮点击后通过AbortController中断fetch请求消息气泡悬浮时显示“复制”按钮点击复制整段内容。这些交互看起来小但每一个都是实际使用中用户感知很强的点尤其“停止生成”这个功能没有它用户面对一个回答时间很长的请求只能干等体验很差。4. 实操过程与完整代码实现4.1 项目初始化和依赖安装我用Vite的Vue TypeScript模板创建项目命令不多说直接列出来npm create vitelatest ai-chat-page -- --template vue-ts cd ai-chat-page npm install npm install pinia element-plus marked highlight.js dompurify npm install -D types/dompurify其中element-plus我按需引入避免全量打包导致首屏体积过大。marked和highlight.js是Markdown渲染和代码高亮用的dompurify做安全清洗。这里我建议一开始就把TypeScript用起来因为聊天消息的数据结构、流式解析的状态类型、API返回类型都比较复杂TS的类型约束在后面的迭代维护中能帮你提前暴露很多问题。4.2 封装一个通用流式请求模块我把流式请求封装成一个独立的组合函数useChatStream让它接收一个消息数组返回一个可迭代的异步生成器。这个设计的好处是无论对接的是什么模型、什么接口格式只要改内部的URL和请求体构造即可组件的业务逻辑完全不用动export function useChatStream() { const controller new AbortController(); async function* streamChat(messages: ChatMessage[]) { const payload { model: gpt-4o-mini, messages: messages.map(({ role, content }) ({ role, content })), stream: true, temperature: 0.7, max_tokens: 2048, }; const response await fetch(/api/chat-stream, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload), signal: controller.signal, }); if (!response.ok) throw new Error(请求失败: ${response.status}); const reader response.body.getReader(); const decoder new TextDecoder(utf-8); let buffer ; while (true) { const { done, value } await reader.read(); if (done) break; buffer decoder.decode(value, { stream: true }); // 按行解析SSE数据 const lines buffer.split(\n); buffer lines.pop() ?? ; for (const line of lines) { const trimmed line.trim(); if (!trimmed.startsWith(data:)) continue; const data trimmed.slice(5).trim(); if (data [DONE]) return; try { const json JSON.parse(data); const delta json.choices?.[0]?.delta?.content ?? json.choices?.[0]?.message?.content ?? json.choices?.[0]?.text ?? ; if (delta) yield delta; } catch { // 解析失败则等待下一段数据拼接后再尝试 } } } } return { streamChat, abort: () controller.abort() }; }4.3 聊天主组件的核心逻辑聊天主组件负责把用户输入、流式生成、消息渲染串起来。为了逻辑清晰我按“组合式函数 组件”的方式分层useChat组合函数管理消息状态和流式生成的调用组件只负责模板绑定和事件分发。核心的发送逻辑大概是这样的const messages refChatMessage[]([]); const isStreaming ref(false); async function sendMessage(content: string) { // 先推入用户消息再创建一条空的AI消息 messages.value.push({ id: genId(), role: user, content, createdAt: Date.now(), status: done, }); const aiMsg: ChatMessage { id: genId(), role: assistant, content: , createdAt: Date.now(), status: streaming, }; messages.value.push(aiMsg); isStreaming.value true; // 只需要把最近的若干条消息发给模型控制token const history messages.value.slice(-10).map(m ({ role: m.role, content: m.content, })); try { const { streamChat, abort } useChatStream(); currentAbort abort; for await (const delta of streamChat(history)) { aiMsg.content delta; } aiMsg.status done; } catch (err) { if (err.name AbortError) { aiMsg.status done; } else { aiMsg.status error; } } finally { isStreaming.value false; } }4.4 请求参数如何选temperature、max_tokens别乱调很多人对temperature、max_tokens、top_p这几个参数理解得比较模糊我用自己的实际测试说明一下。temperature控制模型的创造性和随机性取值0到2之间数值越小回答越保守和确定。我实测做聊天助手时0.6到0.8这个区间比较舒服——太低显得机械太高容易胡说。max_tokens限制回复的最大长度我这里设置2048对日常问答和代码解释足够如果你要AI写长文章或完整系统可以调大一些。还有个容易被忽略的参数是top_p它和temperature是互斥的官方建议两个参数只调整其中之一。我日常固定使用temperature不去动top_p。另外为了降低用量成本我调低了一点另一个关键配置max_tokens会同时限制输出上限但不会影响输入长度输入长度是由messages数组的内容决定的。所以我只传最近的10条消息作为上下文既能保证对话的连贯性又能有效控制每次请求的token消耗。4.5 响应式布局与移动端适配AI聊天页面大概率会被用户在手机浏览器上打开所以响应式布局不能拖到最后才做。我的方案很简单桌面端用左右分栏结构左侧会话列表宽度固定260px右侧对话区域自适应填充。屏幕宽度小于768px时会话列表默认隐藏通过一个抽屉组件切换展示聊天区占满整个页面。这里有一个移动端特别容易踩的坑输入框和键盘的遮挡问题。iOS Safari和部分安卓浏览器在输入框聚焦时虚拟键盘弹出会导致页面视口高度变化如果聊天区域使用了100vh这样的高度设置页面下半部分会被键盘顶出屏幕外或者出现白底区域。我的解决方案是使用100dvh动态视口高度作为聊天容器的高度单位并且输入框区域用position: fixed固定在底部同时监听visualViewport的尺寸变化来做偏移适配。这套方案在我测试的iPhone和安卓模拟器上都表现稳定。5. 常见问题与排查技巧实录5.1 流式输出突然中断没有报错这个是我开发过程中最头疼的问题。现象是AI回复到一半界面上的流式输出突然停了但页面没有报任何错误。排查后发现两类原因一是后端代理服务器默认有超时时间比如Nginx默认60秒无响应会掐断连接而大模型回答长内容时很容易超过这个时长二是浏览器和后端之间有防火墙或网关设备会主动把长时间保持的“空闲连接”断开。解决办法是在后端代理层把响应超时时间调大我设置为300秒并且关闭对SSE连接的缓冲。如果你用Nginx做转发还需要设置proxy_buffering off;否则响应内容会被Nginx缓存前端拿不到流式效果。把这两个问题解决后流式中断的情况基本没有再出现过。5.2 中文内容出现乱码或截断中文乱码的原因我在上面提过主要是TextDecoder没有启用流式解码模式导致的。另一个问题是中文标点或Emoji被截断成半个字符时前端的JSON解析会报错。我的排查思路是先在浏览器Network面板里看原始响应数据确认服务端返回的chunk本身是否完整。如果服务端返回的就是乱码那就是后端到模型的编码环节出了问题如果返回的原始数据正常但前端渲染有问题那就要重点检查解码和解析逻辑。5.3 页面渲染卡顿尤其在长对话时长对话导致的消息累积和Markdown解析开销会在移动端设备上非常明显。我做了两个优化一是消息列表使用虚拟滚动组件只渲染视口内的消息条目而不是把所有历史消息的DOM都挂在页面上二是Markdown解析增加缓存相同的消息内容如果已经解析过HTML直接复用缓存结果避免滚动时反复解析。经过这两项优化后即使页面里有上百轮对话滚动起来依然流畅。5.4 常见问题速查表问题现象可能原因解决方案流式输出没效果全部内容等待结束才一次性出现后端代理开启了响应缓冲或前端没用ReadableStream读取关闭代理缓冲确认使用response.body.getReader()逐块读取中文乱码TextDecoder未启用流式模式使用new TextDecoder(utf-8, { stream: true })回复中断且无报错代理或网关超时调大后端响应超时时间关闭代理缓冲页面白屏或报XSS相关错误AI返回了不安全HTML内容触发DOMPurify拦截检查渲染链路确认DOMPurify.sanitize被调用快速点击发送导致请求混乱没有中断上一个流式请求使用AbortController在发送新消息时中断旧请求移动端键盘弹出后页面错位使用了100vh未适配动态视口改用100dvh配合监听visualViewport调整布局6. 从能用到好用体验优化与功能扩展思路页面功能完整跑通后我又花了不少时间在体验细节上打磨。首先是“打字指示器”效果——AI正在回复时头像旁边显示一个跳动的三个点动画这会极大缓解用户的等待焦虑。其次是消息出现时的过渡动画新消息加入列表时做一个淡入加位移的效果视觉上更自然。这些细节单独拿出来都不复杂但组合起来决定了这个聊天页面是“像一个真实产品”还是“像一个半成品Demo”。再往远处说AI聊天页面的演进方向非常多。如果你想把项目做成一个真正可用的产品可以考虑加上这几个能力多模态支持在聊天区域支持上传图片让模型理解视觉内容这需要后端额外处理文件上传和编码。Function Calling与工具调用让AI能够调用外部工具比如查询天气、查询数据库、调用业务API页面可以设计一些特殊的工具卡片来展示调用过程和结果。语音输入与播报前端接入Web Speech API或第三方语音服务给聊天增加语音能力这在移动端使用场景中非常实用。知识库问答在请求时通过Embedding检索相关文档片段把它们加入系统提示中能显著提升AI回答的准确性。其中Function Calling是我最近正在重点尝试的方向它让AI聊天页面从一个“问答框”升级为“操作入口”。比如用户说“帮我查一下明天北京到上海的航班”AI会自动调用航班查询工具、解析结果并按照一定格式整理答案前端再把工具调用状态以可视化的方式呈现出来。这个方向对消息数据结构的要求会更高但这也正是AI应用前端的想象空间所在。我在实际操作中最大的体会是AI聊天页面开发看起来像是普通的表单列表页面真正深入进去才发现流式处理、状态机设计、安全合规、性能优化每一个环节都有不少门道。最后再分享一个小技巧调试流式接口时不要一上来就看前端代码先在浏览器Network面板里把原始的响应流完整过一遍确认数据格式没问题再回到代码里排查这样能省掉大量的无效排查时间。这套项目做完之后我对Vue 3在实时交互场景下的表现更有信心了后续如果有新的进展再来分享。本文还有配套的精品资源点击获取