前言
在AI应用开发中,流式输出是提升用户体验的关键——相比等待完整结果返回,实时看到内容逐字生成的交互感更符合人类认知习惯。本文将基于Vue3 + DeepSeek API,逐行拆解核心逻辑,从Vue3响应式基础到流式数据解析,手把手教你实现一个可切换流式/非流式的AI聊天Demo,同时吃透Vue3 ref 响应式、Fetch API、ReadableStream等核心知识点。
一、项目初始化:快速搭建Vue3工程
1. 为什么选Vite?
Vite是目前Vue3生态中最轻量化、高性能的构建工具,相比传统webpack,它的冷启动速度、热更新效率提升数倍,尤其适合快速开发小Demo。
2. 初始化步骤(附核心注释)
# 初始化Vite项目
npm init vite
# 交互选择:Vue → JavaScript(新手友好,无需TS)
# 进入项目目录
cd 你的项目名
# 安装依赖
npm install
# 启动开发服务(默认端口5173)
npm run dev
生成的项目结构中,src/App.vue是核心组件文件,我们所有代码都集中在这里开发。
二、核心代码逐行解析(从脚本到视图)
(一)脚本部分:业务逻辑核心(<script setup>)
<script setup>是Vue3组合式API的语法糖,无需手动注册组件/方法,代码更简洁,是当前Vue3开发的主流写法。
1. 响应式数据定义(ref 核心用法)
// 1. 导入Vue3响应式核心API:ref
import { ref } from 'vue'
// 2. 定义响应式数据(核心:数据变 → 视图自动更)
// 重点:ref包装后是「响应式对象」,而非原始值
// 用于绑定输入框的问题内容(双向绑定)
const question = ref('讲一个喜洋洋和灰太狼的故事,20字')
// 控制是否开启流式输出(绑定复选框)
const stream = ref(true)
// 存储AI返回的内容(单向绑定到页面展示)
const content = ref("")
关键知识点拆解:
ref作用:为基本数据类型(字符串/数字/布尔) 创建响应式包装器(复杂类型推荐reactive,但新手先掌握ref即可);- 脚本中修改:必须通过
.value(如content.value = "新内容"),模板中可直接用(如{{content}}); - 响应式本质:Vue3通过代理(Proxy)监听
ref的.value变化,一旦修改,自动触发模板更新(告别jQuery手动操作DOM的时代)。
2. 核心方法:调用DeepSeek API(askLLM)
const askLLM = async () => {
// 1. 输入校验(用户体验:空问题不请求)
if (!question.value) {
console.log('question 不能为空');
return
}
// 2. 加载状态提示(提升用户体验)
content.value = '思考中...';
// 3. API请求配置(请求行+请求头+请求体)
// 接口地址(DeepSeek官方聊天接口)
const endpoint = 'https://api.deepseek.com/chat/completions';
// 请求头:鉴权 + 数据格式声明
const headers = {
// 环境变量:保护敏感信息(API Key不硬编码)
'Authorization': `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
'Content-Type': 'application/json' // JSON格式请求体必须声明
}
// 4. 发送POST请求(Fetch API异步请求)
const response = await fetch(endpoint, {
method: 'POST',
headers,
body: JSON.stringify({
model: 'deepseek-chat', // DeepSeek固定模型名
stream: stream.value, // 控制是否流式输出(绑定复选框)
messages: [ // 对话消息体(遵循OpenAI格式)
{
role: 'user', // 角色:用户
content: question.value // 问题内容(响应式数据)
}
]
})
})
关键知识点拆解:
-
async/await:处理异步请求,避免回调地狱(Fetch API返回Promise); -
环境变量:Vite中以
VITE_开头的变量会被暴露,需在项目根目录创建.env文件配置:
VITE_DEEPSEEK_API_KEY=你的DeepSeek API Key
- Fetch API:浏览器原生请求API。
3. 流式输出处理(核心难点,逐行注释)
if (stream.value) {
// 清空上次的输出内容(避免新旧内容叠加)
content.value = "";
// 获取流式响应读取器(ReadableStream核心)
// response.body是ReadableStream,getReader()用于逐块读取
const reader = response.body?.getReader();
// 二进制流解码为字符串(解决流式数据的编码问题)
const decoder = new TextDecoder();
let done = false; // 标记流是否读取完成
let buffer = ''; // 缓存不完整的分片数据(关键:处理JSON截断)
// 循环读取流数据,直到done为true
while(!done) {
// 读取下一块数据:解构并重命名done为doneReading(避免变量冲突)
const { value, done: doneReading } = await reader?.read()
done = doneReading; // 更新流结束状态
// 解码二进制数据 + 拼接缓存(处理分片不完整的情况)
// value是Uint8Array格式,decoder.decode转为字符串
const chunkValue = buffer + decoder.decode(value);
buffer = ''; // 清空缓存,准备接收新的不完整数据
// 按换行分割数据,过滤出以data:开头的有效行(DeepSeek流式格式)
// DeepSeek流式返回每行格式:data: {"id":"xxx","choices":[{"delta":{"content":"喜"}}]}
const lines = chunkValue.split('\n').filter(line => line.startsWith('data: '))
// 遍历每一行有效数据
for (const line of lines) {
// 去掉"data: "前缀,获取纯JSON字符串
const incoming = line.slice(6);
// 流结束标记:[DONE](DeepSeek约定的结束符)
if (incoming === '[DONE]') {
done = true;
break;
}
try {
// 解析JSON数据(获取AI生成的单个token)
const data = JSON.parse(incoming);
// 核心:获取逐字生成的内容(delta.content是单个字符/词语)
const delta = data.choices[0].delta.content;
// 拼接内容到响应式数据(模板自动更新,实现逐字渲染)
if (delta) {
content.value += delta;
}
} catch(err) {
// 解析失败:说明当前分片不完整,缓存起来下次拼接
// 例如:某次读取的JSON只到一半,缓存后下次和新数据拼接再解析
buffer += `data: ${incoming}`
}
}
}
}
流式输出核心逻辑总结:
- 用
getReader()逐块读取二进制流; - 用
TextDecoder解码为字符串; - 用
buffer缓存不完整的JSON分片(解决截断问题); - 过滤
data:开头的行,解析出逐字内容并拼接; - 遇到
[DONE]标记流结束。
4. 非流式输出处理(对比理解)
} else {
// 非流式:直接解析完整JSON响应(传统方式)
const data = await response.json();
console.log(data);
// 赋值到响应式数据,模板自动更新
content.value = data.choices[0].message.content;
}
非流式输出是一次性获取完整结果,适合对实时性要求不高的场景,代码更简单,但用户体验不如流式。
(二)模板部分:视图渲染(<template>)
模板是Vue3的声明式视图,通过指令绑定数据和事件,无需手动操作DOM。
<template>
<div class="container">
<!-- 输入区域 -->
<div>
<label>输入:</label>
<!-- v-model:双向绑定question响应式数据 -->
<!-- 输入框内容变化 → question.value自动更新;question.value变化 → 输入框内容自动更新 -->
<input class="input" v-model="question"/>
<!-- 点击事件绑定:调用askLLM方法 -->
<button @click="askLLM">提交</button>
</div>
<!-- 输出区域 -->
<div class="output">
<div>
<!-- v-model:双向绑定stream开关(复选框) -->
<label>Streaming</label>
<input type="checkbox" v-model="stream" />
<!-- 插值表达式:单向绑定content,数据变则视图变 -->
<div>{{content}}</div>
</div>
</div>
</div>
</template>
核心指令解析:
v-model:双向数据绑定,适用于表单元素(输入框、复选框等),是Vue3「数据驱动视图」的核心体现;@click:事件绑定语法糖(等价于v-on:click),点击按钮触发askLLM方法;{{content}}:插值表达式,将响应式数据渲染到DOM中,数据更新时自动刷新。
(三)样式部分:页面美化(<style scoped>)
<style scoped>
* {
margin: 0;
padding: 0;
}
.container {
display: flex;
flex-direction: column;
/* 主轴(垂直):靠左对齐,次轴(水平):顶部对齐 */
align-items: start;
justify-content: start;
height: 100vh; /* 容器高度占满视口 */
font-size: 0.85rem;
}
.input {
width: 200px;
}
button {
padding: 0 10px;
margin-left: 6px;
}
.output {
margin-top: 10px;
min-height: 300px; /* 保证输出区域高度 */
width: 100%;
text-align: left;
}
</style>
关键样式说明:
scoped:样式仅作用于当前组件,避免全局样式污染(Vue3的样式隔离方案);display: flex:弹性布局,快速实现垂直排列的页面结构,无需繁琐的浮动/定位;100vh:让容器占满整个视口高度,保证页面布局完整。
三、运行与测试(新手友好版)
1. 配置API Key
在项目根目录新建.env文件,添加你的DeepSeek API Key(需先在DeepSeek官网申请):
VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
2. 启动项目
npm run dev
访问http://127.0.0.1:5173/,即可看到效果:
- 勾选「Streaming」:点击提交,内容逐字生成(流式输出);
- 取消勾选:点击提交,等待完整结果后一次性展示。
四、核心知识点总结(新手必记)
| 知识点 | 核心作用 |
|---|---|
Vue3 ref | 为基本数据类型创建响应式对象,数据变 → 视图自动更 |
v-model | 表单元素与响应式数据双向绑定 |
| Fetch API | 发送异步请求,支持流式响应处理 |
| ReadableStream | 逐块读取流式响应,实现实时渲染 |
| TextDecoder | 二进制流解码为字符串,解决编码问题 |
| Vite环境变量 | 安全管理敏感信息(如API Key) |
五、扩展与优化方向
- 异常处理增强:增加网络错误、API Key失效、接口限流等场景的提示;
- 样式美化:模拟聊天框样式,区分用户消息和AI消息;
- 历史记录:用数组存储对话历史,支持多轮对话;
- 加载动画:流式输出时添加打字机光标效果。
六、总结
本文基于Vue3 + DeepSeek API,从Vue3响应式基础、API请求配置到流式数据解析,完整拆解了AI流式输出Demo的核心逻辑。重点掌握:
ref响应式数据的使用(脚本.value,模板直接用);v-model双向绑定表单数据;- ReadableStream流式数据的解析(二进制流 → 字符串 → 逐字渲染);
- 环境变量的配置(保护敏感信息)。
这个Demo是前端对接AI接口的典型场景,掌握这些知识点后,你可以轻松适配OpenAI等其他大模型的流式接口,也能理解Vue3数据驱动视图的核心思想,从写代码到懂原理。
希望这篇文章能帮你吃透Vue3流式开发的底层逻辑,新手也能快速上手AI应用开发!