Vue 3 + DeepSeek 流式输出实战:从“干等”到“打字机”体验

写在前面

你问 ChatGPT 一个问题,它不是等你 5 秒再啪一下把答案甩脸上,而是一个字一个字往外蹦。这个体验不是后端送的福利,是前端接的流。

AI 产品给用户的第一印象,往往不是你模型多强,而是"它是不是在动"。用户愿意等 10 秒,但不愿意盯着白屏发 10 秒呆。流式输出(streaming)就是解决这个心理魔法的。

今天用 Vue 3 + Vite + DeepSeek API,手把手搭一个会"打字机"输出的聊天页面。

Vue 3 组件:一个 .vue 文件就是一块乐高

现代前端早就不玩"一个 HTML 文件写到底"了。页面是由一个个组件拼起来的,每个 .vue 文件就是一个组件:

区块 装什么 作用
<template> HTML + 绑定语法 视图层
<script> JS 逻辑 数据、方法、生命周期
<style> CSS 样式

Vue 3 推荐用 <script setup>,把变量、函数直接暴露给模板用,少写一层 return。响应式数据用 ref 包一下:

js

ini 复制代码
import { ref } from 'vue';

const question = ref('讲一个关于两只羊与奶龙的故事');
const stream = ref(false);
const content = ref('');

ref 把普通值变成"响应式状态"------你改 question.value,输入框里的内容跟着动;输入框里的内容变了,question.value 也跟着变。这就是 v-model 的底层。

Vite 脚手架:一分钟搭好项目

bash

bash 复制代码
npm init vite
# 选 Vue → JavaScript
cd stream-demo
npm install
npm run dev

Vite 启动后会跑在 http://localhost:5173,index.html 里只有一个 <div id="app"></div>,main.js 负责把 Vue 应用挂到这个点上:

js

javascript 复制代码
import { createApp } from 'vue';
import App from './App.vue';
import './style.css';

createApp(App).mount('#app');

完整页面代码:一个会流式输出的聊天框

vue

ini 复制代码
<template>
  <div class="container">
    <div>
      <label>输入:</label>
      <input type="text" class="input" v-model="question" />
      <label>
        <input type="checkbox" v-model="stream" />
        流式输出
      </label>
      <button @click="update">提交</button>
    </div>
    <div class="output">
      <div>{{ content }}</div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const question = ref('讲一个关于两只羊与奶龙的故事');
const stream = ref(false);
const content = ref('');

const update = async () => {
  if (!question.value) return;
  content.value = '思考中...';

  const response = await fetch('https://api.deepseek.com/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`,
    },
    body: JSON.stringify({
      model: 'deepseek-v4-flash',
      messages: [{ role: 'user', content: question.value }],
      stream: stream.value,
    }),
  });

  if (stream.value) {
    content.value = '';
    const reader = response.body?.getReader();
    const decoder = new TextDecoder();
    let done = false;

    while (!done) {
      const { value, done: doneReading } = await reader.read();
      done = doneReading;
      if (value) {
        const chunk = decoder.decode(value, { stream: true });
        // 简单版:把 SSE 数据拼上去
        content.value += chunk;
      }
    }
  } else {
    const data = await response.json();
    content.value = data.choices[0].message.content;
  }
};
</script>

非流式 vs 流式:两种姿势差在哪?

js

ini 复制代码
// 非流式:等全量生成完,一次性端上来
const data = await response.json();
content.value = data.choices[0].message.content;

js

ini 复制代码
// 流式:边生成边读,边读边展示
const reader = response.body?.getReader();
const decoder = new TextDecoder();

while (!done) {
  const { value, done: doneReading } = await reader.read();
  done = doneReading;
  content.value += decoder.decode(value, { stream: true });
}
维度 非流式 流式
用户感知 白屏等,焦虑感拉满 字一个个蹦,像有人在打字
首次可见时间 生成完全部才能看 几百毫秒就有内容出现
实现复杂度 response.json() 一行搞定 要 ReadableStream + TextDecoder
适合场景 简单问答、短回答 长文本生成、代码、故事
响应体格式 完整 JSON SSE(server-sent events)数据流

为什么流式体验更好? 因为人类对"等待中"的忍耐力,取决于有没有反馈。一个进度条、一个转圈圈、甚至一个字一个字往外蹦,都能显著降低焦虑感。流式输出就是 AI 聊天产品的进度条。

流式读取的三件套:ReadableStream / getReader / TextDecoder

fetch 的 response.body 是一个 ReadableStream 对象,代表响应体还没读完的数据流。要消费它,需要两步:

API 作用
response.body.getReader() 创建一个 reader,从流里拉数据
reader.read() 异步读取下一块,返回 { value, done }
TextDecoder.decode(value, { stream: true }) 把 Uint8Array 二进制块转成文本

TextDecoder 的第二个参数 { stream: true } 很关键。中文一个字符可能被切成两半,分别落到两个 chunk 里。stream: true 让 decoder 保留未完成的字节,等下一次 chunk 到了再拼。不加这个,中文后半部分容易变成乱码。

SSE 格式返回的数据流里,每一行前面都有 data: 前缀,最后一条是 data: [DONE]。生产环境要逐行解析,过滤掉空行和 [DONE],只取有效内容。上面那段代码为了演示直观,直接把所有 chunk 拼在一起------实际项目中建议包一层解析函数。

环境变量:import.meta.env 是 Vite 的专利

前端代码不能硬编码 API Key,否则打开 F12 人人可见。Vite 会自动读取项目根目录的 .env.local,并把以 VITE_ 开头的变量注入到 import.meta.env:

env

ini 复制代码
VITE_DEEPSEEK_API_KEY=sk-xxxxxxxx

代码里这样取:

js

arduino 复制代码
import.meta.env.VITE_DEEPSEEK_API_KEY

注意:只有 VITE_ 前缀的变量才会被暴露到前端。写成 DEEPSEEK_API_KEY 是拿不到的。

5 个踩坑提醒

1. content.value 和模板没同步。 修改 ref 的值必须用 .value,直接 content = '...' 会断掉响应式,页面不更新。

2. v-model 忘了加 .value。 模板里写 v-model="question" 就够了,但 JS 里读取要 question.value。新手常在 if (!question) 这种地方翻车。

3. 流式输出没关 stream: true 的解析细节。 直接把 SSE 数据拼上去,页面上会带 data: 和 DONE 字样。要解析成纯文本再展示。

4. TextDecoder 没传 { stream: true }。 中文 chunk 被切开,后半截变成乱码。这个参数是流式解码的保险。

5. .env.local 没加到 .gitignore。 API Key 上传 GitHub 是经典社死。创建项目第一件事就是把 .env*.local 写进 gitignore。

写在最后

一个会流式输出的聊天页面,技术点其实不多:Vue 3 响应式 + fetch + ReadableStream + SSE 解析。但它背后有一个产品级的认知:AI 应用的用户体验,一半靠响应速度,一半靠"它正在动"的反馈。

从"等 5 秒看结果"到"边生成边看",中间只差一个 reader。但就是这一个 reader,把 demo 和产品的距离拉近了一大截。

相关推荐
雪芽蓝域zzs1 小时前
第 2 节:基础扩展 - 下拉弹窗常用原生属性
vue.js·elementui
扶风ff2 小时前
新品知识更新太快?用练题簿在线刷题,安排企业培训的小测与复盘
前端·学习·小程序
对空六课2 小时前
支持注意力分析的热力图工具有哪些?
前端·数据库·数据分析
Csvn3 小时前
Vue3 响应式与编译:依赖收集如何升级为节点级靶向更新
前端
星栈4 小时前
pnpm 12 升级实测
前端·javascript
Sirens.4 小时前
Java并发锁详解:六类锁策略与 synchronized 底层原理
java·前端·算法
特创数字科技6 小时前
一个纯本地运行的图片处理工具:压缩 / 裁剪 / 九宫格 / 圆角 / 滤镜 / 拼图 / 水印,终生使用
前端
IT_陈寒6 小时前
Vue的v-if和v-for混用居然是个天坑
前端·人工智能·后端
天衍四九-7 小时前
【无标题】
前端·spring boot·mysql·nginx·docker
广州华水科技7 小时前
2026年单北斗GNSS变形监测系统推荐榜单,解锁GNSS位移监测新高度
前端