🌊 在当今 AI 应用爆发的时代,前端与大语言模型(LLM)的交互体验成为产品成败的关键。本文将深入剖析一个基于 Vue 3 + Vite 的流式对话系统实现方案,涵盖从项目初始化、核心逻辑编写,到流式响应处理、用户体验优化等全部细节。我们将结合代码实例,全面讲解如何利用 HTML5 的流式读取能力,实现“边生成边显示”的丝滑交互效果。
🚀 项目初始化与技术栈选择
现代前端开发追求高效与简洁。本项目采用 Vite 作为构建工具,搭配 Vue 3 的 <script setup> 语法糖,极大简化了组件开发流程。
初始化命令
根据文档提示,可通过以下任一方式创建项目:
# 方法一:使用官方脚手架(推荐)
npm create vite@latest stream-demo --template vue
# 方法二:传统方式
npm init vite@latest
随后安装依赖并启动开发服务器:
cd stream-demo
npm install
npm run dev
此过程会生成一个标准的 Vue + Vite 项目结构,包含 main.js、App.vue 等核心文件。
🧠 核心入口:main.js 与应用挂载
main.js 是整个应用的起点:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
这段代码完成了三件事:
- 从
vue包中引入createApp工厂函数; - 引入全局样式;
- 创建应用实例并将根组件
App.vue挂载到index.html中 id 为app的 DOM 节点上。
💡 注意:
index.html文件内容虽简略(仅含 "demo" 字样),但 Vite 会在构建时自动注入必要的 script 和 link 标签,因此无需手动维护。
🖼️ 主界面设计:App.vue 全解析
App.vue 是本项目的核心,它实现了与 LLM 的完整交互流程。
响应式数据声明
使用 Vue 3 的 ref 创建响应式变量:
const question = ref('讲一个喜洋洋和灰太狼的故事,20字')
const stream = ref(true)
const content = ref("")
question:用户输入的问题;stream:是否启用流式输出(通过复选框控制);content:LLM 返回的内容,用于实时渲染。
📌 关键概念:
ref返回的是一个响应式对象,其内部值通过.value访问(在模板中可省略)。
🤖 LLM 请求函数:askLLM
这是整个应用的“大脑”,负责向 DeepSeek API 发起请求并处理响应。
非流式模式(简单场景)
const data = await response.json();
content.value = data.choices[0].message.content;
等待完整响应后一次性赋值。
流式模式(重点!)
流式处理是提升用户体验的核心:
-
清空上次结果:
content.value = ""; -
获取流式读取器:
const reader = response.body?.getReader(); const decoder = new TextDecoder(); // 将二进制转为 UTF-8 字符串 -
循环读取数据块(chunk) :
while(!done) { const { value, done: doneReading } = await reader?.read() // value 是 Uint8Array 类型的二进制数据 } -
解码与拼接:
const chunkValue = buffer + decoder.decode(value, { stream: true });⚠️ 注意:
{ stream: true }参数确保在数据不完整时不会抛出错误,而是保留未完成的字节到下一次解码。 -
解析 SSE(Server-Sent Events)格式: DeepSeek 的流式响应遵循
data: {...}\n\n格式。我们按行分割并过滤以data:开头的行:const lines = chunkValue.split('\n') .filter(line => line.startsWith('data: ')) -
提取 delta 内容并追加:
for (const line of lines) { const incoming = line.slice(6); // 去掉 "data: " if (incoming === '[DONE]') break; // 流结束标志 const data = JSON.parse(incoming); const delta = data.choices[0].delta.content; if (delta) content.value += delta; } -
缓冲区处理: 若 JSON 解析失败(因数据不完整),将剩余部分存入
buffer,留待下次循环拼接:catch(err) { buffer += `data: ${incoming}` }
🔍 为什么需要 buffer?
网络传输的 chunk 边界与 JSON 对象边界不一定对齐。例如,一个完整的data: {"delta": "hello"}可能被拆成两个 chunk:data: {"delta": "he和llo"}。此时首次解析会失败,必须缓存前半部分,与后续数据合并后再解析。
🎨 用户界面与样式
App.vue 的模板部分提供直观的交互:
<div class="input-section">
<label>输入:</label>
<input class="input" v-model="question"/>
<button @click="askLLM">提交</button>
</div>
<div class="output">
<label>Streaming</label>
<input type="checkbox" v-model="stream" />
<div>{{content}}</div>
</div>
v-model实现双向绑定,同步输入框与question的值;- 复选框控制
stream状态,切换流式/非流式模式。
样式亮点
- 背景图:
background-image: url('./assets/image1.png')提升视觉体验; - 响应式布局:使用
flex布局确保在不同屏幕尺寸下正常显示; - 文字溢出处理:
word-wrap: break-word防止长文本溢出容器。
🧪 组件测试:HelloWorld.vue
虽然主功能在 App.vue,但项目仍包含标准的 HelloWorld.vue 组件,用于验证 HMR(热模块替换) 功能:
<template>
<h1>{{ msg }}</h1>
<button @click="count++">count is {{ count }}</button>
</template>
修改此文件时,Vite 会自动刷新组件而不重载整个页面,极大提升开发效率。
📚 知识延伸:流式输出的核心价值
正如 readme.md 所强调:
AI 负责效率,我们负责灵魂
streaming: true 边生成边返回,没必要 done 后再返回所有结果
为何流式如此重要?
- 降低感知延迟:用户无需等待数秒才能看到第一个字;
- 增强互动感:文字逐字出现,模拟“思考”过程,提升拟人化体验;
- 节省资源:服务端可边生成边发送,减少内存占用。
技术本质
- HTML5 Fetch API 的
response.body.getReader()提供了原生流式读取能力; - TextDecoder 正确处理 UTF-8 编码的多字节字符(如中文),避免乱码;
- SSE 协议 是 LLM 流式输出的事实标准,格式简单可靠。
🛠️ 环境变量与安全
API 密钥通过 Vite 的环境变量机制注入:
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`
需在项目根目录创建 .env 文件:
VITE_DEEPSEEK_API_KEY=your_api_key_here
🔒 安全提示:
.env文件不应提交到版本控制系统,需加入.gitignore。
✅ 总结
本文完整还原了一个 Vue 3 + Vite + DeepSeek LLM 流式对话系统 的构建过程。从项目初始化、响应式数据管理,到复杂的流式二进制处理、用户体验优化,每一步都体现了现代前端工程的最佳实践。通过深入理解 ReadableStream、TextDecoder 和 SSE 协议,开发者不仅能实现本项目功能,更能举一反三,应用于任何需要实时数据流的场景——无论是 AI 对话、日志监控,还是股票行情推送。
未来,随着 WebTransport 等新技术的普及,前端处理二进制流的能力将进一步增强。但无论技术如何演进, “为用户提供即时反馈” 的核心原则永不过时。