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:5173index.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

fetchresponse.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 和产品的距离拉近了一大截。

相关推荐
申行1 小时前
Vue 3 + DYMO Connect Framework 实战:从标签模板到打印服务的完整实现
前端
Yeyu1 小时前
车载多 App 同屏渲染(二):SurfaceControlViewHost 跨进程
前端
樊小肆1 小时前
离谱,每轮请求 25% 的 token,竟在重发模型想完就扔的内心独白
前端·人工智能·agent
小妖现世1 小时前
前端面试复习笔记:React 高频题 + JS 手写题
前端
holidaypenguin1 小时前
Windows 本地 HTTPS 证书生成指南
前端
酷酷的逗逗乐1 小时前
前端转 Agent 开发 · 第六节
前端·程序员
回家吃饭去吧1 小时前
WebAssembly 深度解析:前端性能的最后一块拼图
前端
爱勇宝1 小时前
没有 Fn 键关触控板?我做了一个双击即用的 Windows 小工具
前端·后端·程序员
尤小小1 小时前
Vite-SSG 实践:Vue项目预渲染落地完整方案
前端·vue.js