从零理解流式输出 —— 一个 Vue + DeepSeek 的前端实战

目标读者:正在学习现代前端开发,想理解"流式输出"到底是怎么做出来的。


一、为什么需要流式输出?

你有没有遇到过这种情况:打开一个 AI 聊天应用,输入问题,然后......干等

css 复制代码
用户:讲一个关于中国龙的故事
        ↓
     [等待...等待...等待...]  ← 10秒过去了
        ↓
     [一次性返回全部答案]

这就像去餐厅点菜,厨师做完所有菜才一起端上来------你在前面饿得不行。流式输出(Streaming) 就是做完一道端一道:

erlang 复制代码
用户:讲一个关于中国龙的故事
        ↓
     "在" → "遥远" → "的东方" → "有一条" → "巨龙" → ...
        ↑ 一个字一个字实时显示,像打字机一样

打个比方:

像水流一样,LLM 服务器和 chatbot 客户端之间接根管子,生成的 token 就像水流一样不断流向客户端。


二、项目骨架 ------ 文件都在干什么?

先看一眼项目结构(核心文件):

go 复制代码
stream-demo/
├── index.html              ← 页面入口,"插座"
├── package.json            ← 项目配置,依赖声明
└── src/
    ├── main.js             ← Vue 应用启动,"插头"
    ├── App.vue             ← 根组件,核心逻辑都在这里
    ├── style.css            ← 全局样式(全局重置)
    └── components/
        └── HelloWorld.vue  ← 欢迎页组件(Vite 脚手架自带)

第一步:index.html ------ 一个空壳子

html 复制代码
<div id="app"></div>
<script type="module" src="/src/main.js"></script>

就一个 <div id="app"></div>,空的。它只是个挂载点------Vue 的"地盘"从这里开始。外面的事我不管,里面的一切由 Vue 接管。

第二步:main.js ------ 插上电源

js 复制代码
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')   // 把 App 组件"插"进 #app

一行代码把整个 Vue 应用跑起来------createApp(App).mount('#app'),就像把插头插进插座。

第三步:App.vue ------ 主角登场

这是项目的灵魂文件。一个 .vue 文件分三部分:

xml 复制代码
┌────────────────────────────────┐
│  <template>   你看到的页面      │
│  <script>     数据和逻辑       │
│  <style>      样式和美化       │
└────────────────────────────────┘

这就是 组件化思想------把 HTML、CSS、JS 打包成一个独立的、可复用的业务单元。Facebook 的网页由一万多个组件组成,就像乐高积木一样拼出整个应用。


三、数据绑定 ------ 告别手动操作 DOM

数据绑定的核心理念就是:

template 绑定数据,不需要 DOM 编程。

什么意思?对比一下就清楚了:

js 复制代码
// ❌ 传统做法:手动 DOM 操作(噩梦)
document.getElementById('result').innerHTML = '思考中...';
// 每次数据变了都要手动更新 DOM,100 个地方用到就要写 100 次

// ✅ Vue 数据绑定:声明式,自动同步
const content = ref('');
content.value = '思考中...';  // 改数据就行了,页面自动更新

Vue 在实际代码中提供了多种数据绑定方式:

语法 代码示例 作用
{{ }} 插值 {{ content }} 数据 → 页面显示
v-model 双向 v-model="question" 数据 ⇄ 输入框,双向同步
v-if 条件 v-if="Stream" 根据数据控制元素显示/隐藏

v-model 特别值得多说一句------表单元素是个例外。普通数据显示是单向的(数据 → 页面),但输入框需要把用户的输入传回数据,所以需要双向绑定


四、核心实战 ------ 流式输出是怎么实现的

这就是 App.vueupdate 函数的完整流程:

4.1 非流式模式(Stream = false

javascript 复制代码
用户输入 → 发送请求 → 等待LLM全部生成完 → 一次性返回整个 JSON → 显示
js 复制代码
const data = await response.json();
content.value = data.choices[0].message.content;  // 全部内容一次性赋值

简单粗暴,但等得久。

4.2 流式模式(Stream = true

这是精华部分:

js 复制代码
// 请求体里加上 stream: true
body: JSON.stringify({
  model: 'deepseek-chat',
  messages: [{ role: 'user', content: question.value }],
  stream: true,  // ← 关键!告诉服务器:一个一个 token 发,别攒着
})

// 然后不是 response.json(),而是用"水管"读取
const reader = response.body?.getReader();   // 获取读取器
const decoder = new TextDecoder();            // 二进制 → 文本解码器

let done = false;
while (!done) {                               // 循环读取
  const { value, done: doneReading } = await reader.read();
  done = doneReading;
  if (done) break;
  content.value += decoder.decode(value);     // 读到一点就立马追加显示
}

用图来表示就是:

erlang 复制代码
LLM 服务器                          Chatbot 客户端
   │                                      │
   │──── token1: "在" ──────────────────→ │ content.value += "在"
   │──── token2: "遥远" ────────────────→ │ content.value += "遥远"
   │──── token3: "的" ──────────────────→ │ content.value += "的"
   │──── token4: "东方" ────────────────→ │ content.value += "东方"
   │──── ...继续... ────────────────────→ │ ...不断拼接...
   │──── [DONE] ────────────────────────→ │ 读取结束

核心就两步:

  1. 服务器端 :接受 stream: true,token 生成一个就输出一个
  2. 客户端 :用 ReadableStream 的 reader 不断读取,读到就拼接到 content

因为 content 是响应式数据(ref),所以每次追加内容,页面自动更新------用户就看到打字机一样的效果。


五、完整数据流

把整个项目串起来看,一次完整的对话过程是这样的:

markdown 复制代码
1. 用户输入 "讲一个关于中国龙的故事"
   ↓  v-model 双向绑定
2. question.value = "讲一个关于中国龙的故事"
   ↓  点击提交按钮 @click="update"
3. content.value = "思考中...."  → 页面显示"思考中...."
   ↓
4. fetch() 请求 DeepSeek API
   - URL 来自 .env.local(Vite 自动读取环境变量)
   - 请求体携带 stream: true
   ↓
5. 读取响应体(ReadableStream)
   - decoder 把二进制转成文本
   - while 循环不断 reader.read()
   - 每次读到就 content.value += 新内容
   ↓
6. 页面实时更新 {{ content }}
   → 用户看到打字机般的输出效果

六、技术要点清单

这篇文章覆盖了所有关键概念:

概念 说明
流式输出(Streaming) 一个一个 token 实时推送,不等全部生成完
Vue SFC .vue 文件,template + script + style 三位一体
组件化 页面由组件拼成,不是由 HTML 标签拼成
挂载点 <div id="app"> 是 Vue 的入口锚点
响应式数据 ref() 包裹的数据,变了页面就自动更新
数据绑定 {{ }} 文本插值,v-model 双向绑定,v-if 条件渲染
vite 脚手架工具,帮忙读 .env.local 环境变量
ReadableStream 浏览器 API,逐个读取服务器返回的数据块

七、写在最后

最后用一句话总结:

用户体验的打造,是前端的责任。流式输出是 AI 产品核心体验,也是前端工程师必考内容。

现在回头看,实现流式输出其实并不神秘------就是在请求时加一个 stream: true,然后在响应端用 reader 循环读取。但就是这个看似简单的技术,把 AI 产品的用户体验从"等到焦虑"变成了"丝滑享受" 🎯

相关推荐
智驭未来掌门人1 小时前
如何让你的前端服务在服务器上nohub运行
前端
颜进强1 小时前
Claude Code - 18 从 Figma 到 React:一套让 AI 生成代码"不瞎编"的实战工作流
前端·后端·ai编程
疯狂学习GIS1 小时前
AI Agent 操作 QGIS 实测:Codex 与 Claude Code 能自己出图吗
人工智能·ai·大模型·gis·agent·qgis·codex
塔能物联运维1 小时前
液冷数据中心市场突破900亿元,两相液冷成AI算力散热新方向
大数据·人工智能
Sirius Wu1 小时前
OpenClaw Cron安全约束完整详解
人工智能·安全·aigc
GOWIN革文品牌咨询1 小时前
受众、内容、形式、渠道:B2B营销四象限的结构模型
大数据·人工智能
Z-D-K1 小时前
考验AI的“自我“-AI对《红楼梦》后40回的改写(39)
人工智能·ai·aigc·交互·agi
DO_Community2 小时前
Weaviate 托管数据库现已在 DigitalOcean 上开放公测
数据库·人工智能·开源·向量数据库·weaviate