1. 项目概述与核心价值最近在折腾一个基于 Vue 3 和 TypeScript 的前端聊天应用核心目标是直接对接 OpenAI 的官方 API实现一个功能完整、体验流畅的本地化 ChatGPT 界面。这个项目我称之为 Vue3-TS-ChatGPT。为什么会有这个想法相信很多开发者都遇到过类似的需求需要一个更可控、更私密、且能深度定制的对话界面而不是完全依赖官方网页版。官方服务固然方便但有时我们希望对界面、交互逻辑、数据存储有完全的掌控权或者需要将对话能力集成到自己的产品中。这个项目就是为了解决这些问题而生的。简单来说它就是一个运行在你浏览器里的 ChatGPT 客户端。你填入自己的 OpenAI API Key它就能帮你完成对话并且所有的聊天记录都加密后保存在你本地的浏览器数据库里不会上传到任何第三方服务器。这对于注重隐私或者需要离线查看历史记录的场景来说非常实用。项目采用了 Vue 3 的组合式 API 和 TypeScript 来保证代码的健壮性和开发体验用 Vite 构建以获得极速的热更新UI 方面则用 Tailwind CSS 快速搭建。无论你是前端开发者想学习现代技术栈的实战还是需要一个可二次开发的智能对话前端模板这个项目都值得你仔细研究。2. 技术栈选型与架构解析2.1 前端框架为什么是 Vue 3 TypeScript Vite选择 Vue 3 作为核心框架主要看中了其组合式 API 带来的逻辑组织灵活性。在聊天应用这种交互复杂、状态繁多的场景下组合式 API 允许我们将与“对话流”、“消息处理”、“用户配置”相关的逻辑抽离成独立的组合式函数代码的可读性和可维护性远胜于 Vue 2 的选项式 API。例如处理流式响应、管理聊天上下文这些功能都能封装成清晰的useChatStream、useChatContext函数。TypeScript 的加入则是为项目上了“保险”。OpenAI API 的请求参数和响应结构都比较复杂使用 TypeScript 可以明确定义接口比如ChatCompletionRequestMessage、StreamingChatCompletionChunk等。这在开发时能获得精准的代码提示和类型检查避免因属性名拼写错误或类型不匹配导致的运行时bug尤其是在处理异步流数据时类型安全至关重要。构建工具选用 Vite没什么好犹豫的。它的开发服务器启动速度和热更新速度远超传统的 Webpack对于需要频繁调试界面和交互的聊天应用来说能极大提升开发效率。Vite 对 Vue 3 和 TypeScript 的支持也是开箱即用生态契合度完美。2.2 样式与 UITailwind CSS 的效用主义项目采用 Tailwind CSS 进行样式开发。在需要快速迭代、且组件样式多变的聊天界面中Tailwind 的效用优先Utility-First理念优势明显。我们不需要为每个消息气泡、输入框、按钮单独编写 CSS 类名和样式文件直接在模板中组合工具类即可快速实现设计。例如一个用户消息气泡可能只需要bg-blue-100 p-4 rounded-lg几个类。这减少了上下文切换也让样式与结构更紧密。同时通过配置tailwind.config.js可以轻松定制出符合项目品牌色的设计系统。2.3 核心功能库的深度考量OpenAI API 与流式响应 (fetch): 直接使用浏览器原生的fetch API调用 OpenAI 的/v1/chat/completions接口并设置stream: true以启用流式传输。这是体验的关键它允许答案像官方 ChatGPT 那样逐字打出而不是等待整个响应完成再显示。处理ReadableStream是这里的核心技术点。Markdown 渲染与代码高亮 (markedhighlight.js): AI 的回答经常包含代码块和 Markdown 格式。marked库负责将 Markdown 文本安全地转换为 HTML。安全是关键必须配置sanitize: false但启用sanitizer函数或使用DOMPurify后处理以防止 XSS 攻击。highlight.js则用于对转换后 HTML 中的code块进行语法高亮提升代码可读性。数据持久化与加密 (IndexedDBCryptoJS): 聊天记录是核心资产。使用IndexedDB进行本地存储因为它存储容量大远高于localStorage且支持异步操作不阻塞主线程。封装ChatStorageManager类来统一管理聊天会话、消息的增删改查。更关键的是用户的 OpenAI API Key 极其敏感。项目使用CryptoJS.AES进行对称加密后再存储。密钥由用户前端生成并保管理论上任何后端都无法解密这实现了 API Key 的“前端零知识存储”极大增强了安全性。工具库 (lodash): 选用lodash主要是为了其稳定、功能丰富的工具函数比如debounce用于搜索防抖、cloneDeep深度复制聊天记录对象等能避免重复造轮子提升开发效率。路由 (vue-router): 用于管理不同的视图例如主聊天界面、历史记录列表页、设置页面等实现单页面应用SPA的流畅导航体验。注意整个技术栈的选择都围绕“现代”、“高效”、“安全”和“良好开发者体验”展开。没有选用过于重型或封装过度的库以保证项目的轻量和可控性。3. 核心模块实现细节拆解3.1 流式对话引擎的实现这是项目的灵魂。与普通 HTTP 请求不同流式请求需要处理持续到达的数据块。实现步骤构造请求使用fetch向https://api.openai.com/v1/chat/completions发起 POST 请求。请求头需包含Authorization: Bearer ${apiKey}和Content-Type: application/json。请求体是关键必须设置stream: true并将messages数组包含上下文对话发送过去。处理流式响应响应体的body是一个ReadableStream。我们需要创建一个reader来读取这个流。const response await fetch(endpoint, options); const reader response.body?.getReader(); if (!reader) throw new Error(Stream not available);循环读取与解析在一个while循环中不断读取数据块 (reader.read())。每个数据块是 Uint8Array 格式需要解码为字符串。OpenAI 的流式响应以data:开头每行是一个独立的 JSON 对象或[DONE]标记。const decoder new TextDecoder(); while (true) { const { done, value } await reader.read(); if (done) break; // 流结束 const chunk decoder.decode(value); // 按行分割处理每一行 const lines chunk.split(\n).filter(line line.trim() ! ); for (const line of lines) { if (line data: [DONE]) { // 对话结束 return; } if (line.startsWith(data: )) { const jsonStr line.slice(6); // 去掉 data: try { const parsed JSON.parse(jsonStr); const content parsed.choices[0]?.delta?.content || ; // 将 content 追加到当前回答中 onDataChunk(content); } catch (e) { console.error(解析流数据失败:, e); } } } }实时更新 UI通过onDataChunk回调函数通常是一个 Vue 的ref变量的更新函数将每次获取到的content片段追加到界面上正在生成的回答中实现打字机效果。实操心得错误处理要周全网络中断、API Key 失效、模型过载等都会导致流异常。必须在try...catch中包裹核心逻辑并在finally块中确保reader.releaseLock()被调用释放流锁。上下文管理发送给 API 的messages数组需要包含足够的历史对话以实现连贯的上下文。通常的做法是维护一个数组每次新对话时将用户问题和 AI 的完整回答依次加入。但要注意 OpenAI API 有 Token 数量限制需要实现一个“滑动窗口”或总结机制防止超出上限。性能优化频繁的 DOM 更新每收到一个词就更新一次可能影响性能。可以采用“节流”更新策略例如累积一小段文本如每100毫秒或每5个字符再更新一次 UI以平衡实时性和流畅度。3.2 聊天数据管理器的封装ChatStorageManager类是对IndexedDB操作的抽象目标是提供一套 Promise 化的、易于使用的 API。核心设计数据库与表Object Store设计数据库名:ChatDB版本: 初始为1结构升级时递增。表1sessions: 存储聊天会话。主键为自增id字段包括title会话标题通常取第一条消息摘要、createdAt、updatedAt。表2messages: 存储单条消息。主键为自增id字段包括sessionId关联会话、roleuser/assistant、content、timestamp。需要为sessionId创建索引以便快速查询某个会话下的所有消息。类方法封装initDB(): 打开或创建数据库在onupgradeneeded事件中创建或升级表结构。createSession(title): 创建新会话返回会话ID。addMessage(sessionId, role, content): 向指定会话添加一条消息同时更新该会话的updatedAt时间。getMessagesBySessionId(sessionId): 获取一个会话的所有消息按timestamp排序。getAllSessions(): 获取所有会话列表按updatedAt倒序排列。deleteSession(sessionId): 删除一个会话及其所有关联消息需要事务。注意事项异步操作IndexedDB 所有操作都是异步的。封装时务必返回 Promise方便在 Vue 组件中使用async/await。事务使用对于读写操作尤其是涉及多个表的操作如删除会话要使用事务来保证数据一致性。错误处理对数据库打开失败、读写失败等情况要有降级或用户提示机制。例如可以尝试 fallback 到localStorage容量有限或提示用户检查浏览器是否禁用了 IndexedDB。3.3 API Key 的安全加密存储安全是重中之重。绝不能明文存储 API Key。实现流程生成加密密钥当用户首次输入 API Key 时前端生成一个随机的盐Salt和密码Passphrase。这个密码不会存储而是由用户自己记忆或由密码管理器保存。项目使用CryptoJS.PBKDF2函数结合用户密码和盐派生出一个固定长度的加密密钥。import CryptoJS from crypto-js; const salt CryptoJS.lib.WordArray.random(128/8); // 生成随机盐 const key CryptoJS.PBKDF2(userPassphrase, salt, { keySize: 256/32, iterations: 1000 });加密 API Key使用上面派生的密钥通过 AES 算法加密用户的明文 API Key。const encryptedSK CryptoJS.AES.encrypt(plainTextSK, key.toString(), { iv: iv }).toString();存储密文与盐将加密后的密文encryptedSK和随机生成的salt以及可能用到的初始化向量iv一起存入IndexedDB或localStorage。注意派生密钥用的密码Passphrase不存储。解密使用当需要调用 API 时请求用户输入密码。用该密码和存储的盐重新计算派生密钥然后解密出原始的 API Key 用于本次请求。解密后API Key 应仅保存在内存中用完即弃。重要提示这种前端加密方案的安全性建立在用户密码的强度上。它防止了数据库内容被直接窃取后导致的 API Key 泄露但无法防范客户端本身的恶意代码如 XSS 攻击。因此这更多是一种“深度防御”策略和隐私增强手段。最安全的做法仍然是使用后端代理服务器来中转请求完全避免在前端暴露 API Key。本项目方案适用于对隐私有要求、且信任前端代码环境的个人或内部项目。4. 前端界面与交互构建实录4.1 使用 Vue 3 组合式函数组织逻辑我们将聊天相关的响应式状态和逻辑封装进一个useChat组合式函数中。// composables/useChat.ts import { ref, computed } from vue; import { streamChatCompletion } from /api/openai; import { useChatStorage } from /composables/useChatStorage; export function useChat() { const storage useChatStorage(); const messages refChatMessage[]([]); const currentSessionId refnumber | null(null); const isLoading ref(false); const inputText ref(); // 发送消息 const sendMessage async () { if (!inputText.value.trim() || isLoading.value) return; const userMessage: ChatMessage { role: user, content: inputText.value }; messages.value.push(userMessage); // 存储用户消息 if (currentSessionId.value) { await storage.addMessage(currentSessionId.value, user, inputText.value); } const prompt inputText.value; inputText.value ; isLoading.value true; // 准备助手消息占位 const assistantMessage: ChatMessage { role: assistant, content: }; messages.value.push(assistantMessage); let fullContent ; try { await streamChatCompletion({ messages: [...messages.value.slice(0, -1)], // 发送历史消息不包括刚添加的占位消息 onChunk: (chunk) { fullContent chunk; // 直接更新最后一个消息对象的内容Vue 会响应式更新 DOM assistantMessage.content fullContent; }, onDone: async () { // 流式结束存储完整的助手消息 if (currentSessionId.value) { await storage.addMessage(currentSessionId.value, assistant, fullContent); } isLoading.value false; }, onError: (error) { console.error(Stream error:, error); assistantMessage.content \n\n[错误: ${error.message}]; isLoading.value false; } }); } catch (error) { isLoading.value false; // 处理错误 } }; // 创建新会话 const createNewSession async () { const sessionId await storage.createSession(新对话); currentSessionId.value sessionId; messages.value []; }; // 加载历史会话 const loadSession async (sessionId: number) { const sessionMessages await storage.getMessagesBySessionId(sessionId); messages.value sessionMessages; currentSessionId.value sessionId; }; return { messages, inputText, isLoading, sendMessage, createNewSession, loadSession, // ... 其他状态和方法 }; }在 Vue 组件中我们可以直接解构使用这些响应式状态和方法逻辑非常清晰。4.2 基于 Tailwind CSS 的聊天界面搭建聊天界面主要分为三部分侧边栏会话列表、主聊天区域、底部输入框。主聊天区域 (ChatWindow.vue)使用flex布局垂直排列消息。每条消息根据role决定对齐方式用户消息居右助手消息居左。消息气泡样式通过 Tailwind 类控制圆角、内边距、背景色、最大宽度等。助手消息的内容content是一个 Markdown 字符串需要安全地渲染为 HTML。template div classflex-1 overflow-y-auto p-4 space-y-4 div v-for(msg, index) in messages :keyindex :class[flex, msg.role user ? justify-end : justify-start] div :class[max-w-[70%] rounded-lg px-4 py-2, msg.role user ? bg-blue-500 text-white : bg-gray-100 text-gray-800] !-- 使用 v-html 渲染 Markdown需确保内容安全 -- div v-ifmsg.role assistant classprose prose-sm max-w-none v-htmlrenderedMarkdown(msg.content)/div div v-else{{ msg.content }}/div /div /div div v-ifisLoading classflex justify-start div classbg-gray-100 rounded-lg px-4 py-2 max-w-[70%] div classflex space-x-1 div classw-2 h-2 bg-gray-400 rounded-full animate-bounce styleanimation-delay: -0.32s;/div div classw-2 h-2 bg-gray-400 rounded-full animate-bounce styleanimation-delay: -0.16s;/div div classw-2 h-2 bg-gray-400 rounded-full animate-bounce/div /div /div /div /div /template script setup import { marked } from marked; import hljs from highlight.js; import highlight.js/styles/github.css; // 引入代码高亮样式 import DOMPurify from dompurify; // 引入安全净化库 // 配置 marked marked.setOptions({ highlight: function(code, lang) { const language hljs.getLanguage(lang) ? lang : plaintext; return hljs.highlight(code, { language }).value; }, breaks: true, gfm: true, }); const renderedMarkdown (content) { // 1. 将 Markdown 转换为 HTML const rawHtml marked.parse(content); // 2. 使用 DOMPurify 进行净化防止 XSS const cleanHtml DOMPurify.sanitize(rawHtml); return cleanHtml; }; /script关键点v-html与安全直接使用v-html渲染用户或 AI 提供的内容是危险的。必须使用DOMPurify这样的库对marked生成的 HTML 进行净化移除所有潜在的恶意脚本。代码高亮集成在marked的配置中通过highlight选项调用highlight.js进行高亮。需要提前引入高亮样式文件。加载状态指示器使用简单的 CSS 动画实现三个点的跳动效果提升等待时的用户体验。4.3 状态管理与路由集成项目状态管理相对简单没有引入 Pinia 或 Vuex而是利用组合式函数和provide/inject在组件间共享状态。根组件 (App.vue)使用useChat创建聊天状态并通过provide提供给下层组件如侧边栏、聊天窗口。侧边栏组件 (Sidebar.vue)通过inject获取会话列表和加载会话的方法。路由 (vue-router)配置两个主要路由/主聊天界面和/settings设置页面用于管理 API Key。路由切换时通过导航守卫或组件生命周期钩子来保存/加载当前聊天状态。5. 开发、构建与部署全流程5.1 本地开发环境搭建克隆项目:git clone repository-url cd Vue3-TS-ChatGPT安装依赖:npm install # 或 yarn install # 或 pnpm install配置环境变量在项目根目录创建.env.development文件。虽然 API Key 由用户前端输入但可以在这里配置一些开发时的默认值或代理设置如果需要。# .env.development VITE_APP_TITLEVue3 ChatGPT (Dev) # 如果需要本地代理解决跨域问题 # VITE_OPENAI_API_BASEhttp://localhost:3000/api/proxy启动开发服务器:npm run devVite 会启动一个本地服务器通常是http://localhost:5173并打开浏览器。热重载HMR功能让你在修改代码后能即时看到变化。5.2 生产环境构建与优化构建命令:npm run build这个命令会调用 Vite 的构建模式进行 TypeScript 编译、代码压缩、Tree Shaking 等优化最终在dist目录生成静态文件。构建优化要点代码分割Vite 默认会对动态导入进行代码分割。可以检查vite.config.ts确保rollupOptions.output.manualChunks配置合理将vue、marked、highlight.js等较大的第三方库单独打包利用浏览器缓存。压缩与混淆Vite 使用 Terser 进行 JS 压缩CSS 压缩则由cssnano处理。确保生产构建时这些选项是开启的。环境变量创建.env.production文件注入生产环境特定的变量如VITE_APP_TITLE。部署生成的dist文件夹内容是完全静态的可以部署到任何静态网站托管服务上例如Vercel/Netlify直接关联 Git 仓库自动部署。GitHub Pages使用gh-pages工具或 GitHub Actions 自动化部署。传统服务器将dist文件夹上传到 Nginx 或 Apache 的 Web 目录下即可。5.3 配置详解与自定义项目的主要配置文件是vite.config.ts和tailwind.config.js。vite.config.ts关键配置:import { defineConfig } from vite; import vue from vitejs/plugin-vue; import { resolve } from path; export default defineConfig({ plugins: [vue()], resolve: { alias: { : resolve(__dirname, src), // 设置 别名指向 src 目录 }, }, server: { port: 5173, // 开发服务器端口 proxy: { // 配置开发服务器代理解决跨域问题如果直接调用 OpenAI API 遇到 CORS 错误 /api: { target: https://api.openai.com, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), }, }, }, build: { outDir: dist, sourcemap: false, // 生产环境关闭 sourcemap 以保护代码 rollupOptions: { output: { manualChunks: { vendor-vue: [vue, vue-router], vendor-ui: [tailwindcss], vendor-markdown: [marked, highlight.js, dompurify], vendor-crypto: [crypto-js], }, }, }, }, });tailwind.config.js自定义:/** type {import(tailwindcss).Config} */ export default { content: [ ./index.html, ./src/**/*.{vue,js,ts,jsx,tsx}, // 确保扫描所有 Vue/TS 文件 ], theme: { extend: { colors: { primary: #10a37f, // 可以定义 ChatGPT 风格的主题色 }, animation: { bounce-dot: bounce 1.4s infinite ease-in-out both, // 自定义加载动画 }, }, }, plugins: [], }6. 常见问题排查与性能调优6.1 流式响应中断或显示不全现象回答生成到一半突然停止或者最后一部分内容丢失。排查步骤检查网络打开浏览器开发者工具的“网络”(Network)标签页查看对 OpenAI API 的请求。确认请求状态是否为 200并查看“响应”(Response)内容。流式响应的数据应该是分很多个data: {...}块。检查 API Key 与额度确保 API Key 有效且未过期账户余额充足。可以在 OpenAI 控制台查看使用情况。审查流处理逻辑在streamChatCompletion函数中添加详细的日志打印每个接收到的数据块(chunk)和解析后的内容(content)。确认onChunk回调被正确、连续地调用。Token 超限如果上下文消息太长可能超过模型的最大 Token 限制如gpt-3.5-turbo的 4096 tokens会导致请求被 API 拒绝或截断。需要在发送请求前计算 Token 数或实现自动截断历史消息的策略。错误处理遗漏确保reader.read()循环中以及fetch请求本身都有完善的try...catch并将错误信息反馈到 UI而不是静默失败。6.2 IndexedDB 操作失败或数据丢失现象聊天记录无法保存或刷新页面后会话消失。排查步骤浏览器兼容性与权限确认浏览器支持 IndexedDB现代浏览器都支持。检查浏览器是否设置了“阻止第三方 Cookie 和网站数据”这可能会影响 IndexedDB。数据库版本升级问题如果你修改了ChatStorageManager中数据库的表结构比如新增字段但没有正确处理onupgradeneeded事件可能导致旧版本数据库无法打开或数据无法读取。确保版本号递增并在升级回调中创建新的表或索引。异步时序问题IndexedDB 操作是异步的。确保在数据完全写入后再进行读取或页面跳转。例如在sendMessage函数中等待storage.addMessage的 Promise 完成后再更新 UI 状态虽然为了流畅性UI 可以先更新。存储空间不足IndexedDB 有存储上限但通常很大几十 MB 到几百 MB。如果对话极多可能触顶。可以增加清理旧会话的功能。6.3 页面加载缓慢或交互卡顿现象首次打开页面白屏时间长或发送消息时界面反应迟钝。优化策略代码分割与懒加载利用 Vite 的动态导入 (import()) 实现路由懒加载和组件懒加载。例如将“设置”页面这样的非核心路径单独打包只在访问时加载。// router/index.ts const Settings () import(/views/Settings.vue);第三方库按需引入highlight.js和lodash可能体积较大。确保只引入需要的部分。对于highlight.js可以只引入常用的语言包。import hljs from highlight.js/lib/core; import javascript from highlight.js/lib/languages/javascript; import python from highlight.js/lib/languages/python; hljs.registerLanguage(javascript, javascript); hljs.registerLanguage(python, python);对于lodash使用lodash-es并配合 Tree Shaking或者直接导入具体函数import debounce from lodash/debounce。虚拟列表优化如果单次会话消息数量非常多比如上千条渲染所有 DOM 节点会导致性能下降。可以考虑使用虚拟列表组件如vue-virtual-scroller只渲染可视区域内的消息。Markdown 渲染性能marked解析和highlight.js高亮都是 CPU 密集型操作。对于很长的 AI 回答可以将其拆分成多个段落分批进行渲染避免长时间阻塞主线程。6.4 安全与跨域问题现象在浏览器中直接调用api.openai.com时控制台出现 CORS (跨域资源共享) 错误。解决方案开发环境代理如上文vite.config.ts所示配置开发服务器的代理将/api路径的请求转发到 OpenAI避免浏览器直接跨域。生产环境后端代理强烈推荐前端直接暴露 API Key 即使加密也存在风险。最佳实践是部署一个简单的后端服务可以用 Node.js、Python、Go 等编写前端将请求发送到自己的后端由后端添加 API Key 并转发给 OpenAI。这样 API Key 完全保存在安全的服务器环境也彻底解决了 CORS 问题。本项目前端可以很容易地修改api模块的请求地址指向你自己的后端端点。内容安全策略 (CSP)如果部署后出现样式或脚本加载问题可能需要配置正确的 CSP 响应头。确保允许unsafe-inline样式Tailwind 生成大量工具类或self来源的脚本。6.5 功能扩展与自定义建议这个项目是一个很好的起点你可以基于它进行深度定制多模型支持除了 GPT-3.5/GPT-4可以扩展支持 Claude、Gemini 等模型的 API在 UI 上提供模型切换选项。对话功能增强消息编辑与重发允许用户编辑上一条消息重新发送AI 会基于新消息重新生成回答。对话分支对某条 AI 回答不满意时可以从此处创建新的分支对话探索不同方向。系统指令预设提供一些常用的系统角色指令如“编程助手”、“创意写手”模板方便用户切换。UI/UX 优化主题切换实现深色/浅色模式。消息复制与分享为每条消息添加复制按钮或生成分享链接。快捷键支持支持CtrlEnter发送、CtrlK聚焦输入框等。数据管理导入/导出支持将会话记录导出为 JSON 或 Markdown 文件并支持导入恢复。搜索功能在所有历史对话中全文搜索消息内容。自动摘要为新会话自动生成一个标题可以用第一条消息或 AI 生成。这个项目把我对现代前端开发中流式处理、状态管理、本地存储和安全性的思考都融了进去。实际用下来最深的体会是细节决定体验。比如流式响应那里一个错误处理没写好整个对话就可能卡住IndexedDB 的事务没用好数据就可能不一致。前端加密 API Key 更像是一种心理安慰和隐私门槛真正要用于生产还是得靠后端代理。不过作为一个学习项目和可高度定化的个人工具它已经足够出色了。如果你也在构建类似的应用希望这些踩坑经验和实现细节能帮你省点时间。