详情

首页手游攻略 不用第三方SDK!Vue3原生Fetch实现DeepSeek流式输出,90%前端都会踩的分片解析坑一次性解决

不用第三方SDK!Vue3原生Fetch实现DeepSeek流式输出,90%前端都会踩的分片解析坑一次性解决

佚名 2026-09-01 18:24:54

不用第三方SDK!Vue3原生Fetch实现DeepSeek流式输出,90%前端都会踩的分片解析坑一次性解决并不只看表面做法,关键还要理解相关条件、限制和后续影响。

前言

做AI聊天页面你一定遇过两种糟心体验:

  1. 关闭流式:点击提交黑屏等待3-10秒,一次性弹出全文,用户等待感极强
  2. 手写SSE流式:网络分包截断JSON,疯狂报JSON.parse解析失败,文字丢失乱码

网上很多示例只给极简demo,没有处理分片容错,上线必崩。这篇内容以Vite+Vue3+原生Fetch完整实现DeepSeek对话,同时支持流式打字机/一次性返回双模式,自带buffer分片容错逻辑,看完你能学到:

  1. SSE流式输出底层二进制流传输原理
  2. ReadableStream、TextDecoder浏览器原生API完整用法
  3. buffer缓冲区解决TCP分包截断JSON的核心方案
  4. 流式/非流式接口两套分支代码完整实现
  5. 开发高频踩坑清单+修复方案,直接规避线上bug
  6. 可直接复制运行的完整单文件组件

一、先搞懂:什么是LLM流式SSE输出

1.1 传统一次性请求(stream=false)

后端等AI完整生成全部文本,组装成完整JSON一次性返回。前端调用response.json()直接解析,优点代码简单,缺点等待时间长,交互割裂。

1.2 SSE流式请求(stream=true)

大模型每生成一段Token,就封装成data: JSON格式通过二进制流实时推送到前端:

  1. 传输载体:response.body 二进制可读流(Uint8Array字节数组)
  2. 分隔规则:每条数据用换行n分割,结尾单独发送data: [DONE]标识流结束
  3. 传输痛点:TCP网络分包会把一条完整JSON拆成两半,直接解析报错,必须用buffer缓存残缺片段

1.3 核心API介绍

  1. response.body.getReader():创建流读取器,逐块拉取二进制数据
  2. TextDecoder():二进制Uint8Array转UTF-8字符串,解决中文乱码
  3. buffer缓冲区:存储上一轮未解析完成的残缺data:报文,下一轮拼接完整再解析

二、项目前置环境配置

2.1 依赖无需额外安装

本方案纯浏览器原生API,不需要openai/langchain等第三方SDK,Vite Vue3项目开箱即用。

2.2 环境变量配置(关键,防止密钥硬编码泄露)

项目根目录新建.env文件,填入DeepSeek密钥:

VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxx

Vite通过import.meta.env.VITE_XXX读取环境变量,打包后不会明文暴露密钥。

三、完整可运行代码 App.vue

<script setup>import { ref } from 'vue'// 响应式状态const question = ref('讲一个中国龙的故事'); // 用户输入提问const content = ref(''); // AI输出内容const stream = ref(true); // 是否开启流式输出开关// 核心请求函数const update = async () => {// 空提问拦截,避免无效请求if (!question.value) return;content.value = '思考中...';// DeepSeek对话接口地址const endpoint = 'https://api.deepseek.com/chat/completions';const headers = {'Content-Type': 'application/json',Authorization: `Bearer ${import.meta.env.VITE_DEEPSEEK_API_KEY}`};// 发起POST请求const response = await fetch(endpoint, {method: 'POST',headers,body: JSON.stringify({model: 'deepseek-v4-flash',messages: [{ role: 'user', content: question.value }],stream: stream.value // 动态控制流式开关})})// ========== 分支1:流式输出(打字机效果,本文核心) ==========if (stream.value) {content.value = ""; // 清空思考中占位文字// 获取二进制流读取器const reader = response.body?.getReader();// 二进制转UTF8文本解码器const decoder = new TextDecoder();let done = false; // 流读取完成标记let buffer = ''; // 残缺分片缓存(解决JSON截断报错核心)// 循环持续拉取二进制分片while (!done) {// 异步读取一块二进制数据const { value, done: doneReading } = await reader?.read();done = doneReading;// 拼接上一轮残留残缺片段 + 当前新解码文本const chunkValue = buffer + decoder.decode(value);buffer = ""; // 缓存已合并,清空等待下一轮残缺数据// 按换行分割文本,过滤仅保留data:开头的SSE有效行const lines = chunkValue.split('n').filter((line) => line.startsWith('data: '))// 逐行解析每条SSE报文for (const line of lines) {// 切掉前缀 data: 6个字符,获取纯JSON/结束标识const incoming = line.slice(6);// 检测到结束标识,终止全部循环if (incoming === '[DONE]') {done = true;break;}try {// 解析JSON字符串const data = JSON.parse(incoming);// 流式专属增量文本deltaconst delta = data.choices[0].delta.content;// 存在增量文字则追加到页面,实现打字机效果if (data && delta) {content.value += delta;}} catch (err) {// JSON解析失败=分片不完整,存入buffer下一轮拼接buffer = `data: ${incoming}`;}}}}// ========== 分支2:非流式一次性返回 ==========else {const data = await response.json();// 非流式使用message完整文本,而非delta增量content.value = data.choices[0].message.content;}}</script><template><p class="container"><!-- 提问输入区域 --><p><label>输入:</label><input class="input" v-model="question" /><button @click="update">提交</button></p><!-- 流式开关 + AI回答展示区 --><p class="output"><p><label>Streaming流式输出</label><input type="checkbox" v-model="stream" /></p><p>{{ content }}</p></p></p></template><style scoped>.container {display: flex;flex-direction: column;align-items: flex-start;justify-content: flex-start;height: 100vh;font-size: 0.85rem;padding: 20px;}.input {width: 300px;padding: 4px 8px;}.output {margin-top: 12px;min-height: 300px;width: 100%;text-align: left;line-height: 1.6;}button {padding: 4px 12px;margin-left: 8px;cursor: pointer;}</style>

四、核心流式逻辑逐行深度拆解

4.1 基础变量初始化

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

  1. reader:流专属读取器,串行读取二进制数据,保证顺序不乱
  2. decoder:全局解码器,循环内复用,避免中文跨分片乱码
  3. done:外层while循环开关,控制数据流是否全部接收完毕
  4. buffer:全文最关键容错变量,专门存储被TCP分包截断的半条data:报文

4.2 while循环:持续拉取二进制分片

while (!done) {const { value, done: doneReading } = await reader?.read();done = doneReading;const chunkValue = buffer + decoder.decode(value);buffer = "";const lines = chunkValue.split('n').filter((line) => line.startsWith('data: '))}

  1. reader.read():异步阻塞读取,有新分片立刻返回,无数据持续等待
  2. chunkValue = buffer + 新文本:核心容错操作,把上一轮残缺片段和本次新数据拼接,保证报文完整
  3. split('n'):SSE协议每条数据换行分隔,切割后过滤无效空行、心跳包,只保留data:有效数据

4.3 for循环:解析单条SSE报文

for (const line of lines) {const incoming = line.slice(6);if (incoming === '[DONE]') {done = true;break;}try {const data = JSON.parse(incoming);const delta = data.choices[0].delta.content;if (data && delta) content.value += delta;} catch (err) {buffer = `data: ${incoming}`;}}

  1. line.slice(6):剔除data: 固定前缀,提取纯JSON字符串
  2. [DONE]:服务端流结束标志,终止所有循环
  3. delta.content:流式接口专属增量字段,每次仅返回本次生成的少量文字,Vue响应式追加实现逐字打字效果
  4. catch容错逻辑:JSON解析报错代表当前行是残缺报文,存入buffer,下一轮循环拼接新分片后再解析,杜绝文字丢失

4.4 非流式分支简单说明

else {const data = await response.json();content.value = data.choices[0].message.content;}

关闭流式时,后端等待AI全部生成完毕,一次性返回完整JSON,使用message.content完整文本,无需处理二进制流、分片、buffer,代码极简,但用户等待体验差。

五、高频开发踩坑清单(必看,上线避坑)

坑1:TCP分包截断JSON,疯狂报parse错误

现象:控制台频繁抛出JSON语法错误,AI回答文字残缺、丢失原因:网络传输会把一条data: JSON切成两块,单块无法完整解析解决方案:代码中buffer缓冲区,拼接残缺片段后再解析

坑2:中文跨分片解码出现乱码

现象:部分中文显示问号、乱码字符优化方案:decoder.decode(value, { stream: true }),解码器自动缓存跨分片字节,完整解析中文

坑3:忘记清空buffer,重复叠加文本

现象:AI回答重复、内容翻倍修复:拼接chunkValue后立刻执行buffer = ""清空缓存

坑4:混淆流式/非流式字段 delta / message

现象:关闭流式返回undefined,开启流式无文字输出区分:

  1. stream=true → data.choices[0].delta.content
  2. stream=false → data.choices[0].message.content

坑5:连续点击提交,多请求文字叠加错乱

优化补充:增加loading锁,请求期间禁用提交按钮,防止并发请求

坑6:API Key硬编码写在代码内

风险:前端打包后源码泄露密钥,产生高额扣费规范:统一放入.env环境变量,通过import.meta.env读取

六、流式与非流式方案对比

对比维度stream=true 流式SSEstream=false 一次性返回
传输方式二进制分片持续推送完整JSON单次返回
解析逻辑ReadableStream+buffer容错直接response.json()
输出字段delta.content(增量小段)message.content(全文)
用户体验边生成边展示,低等待感知等待全部生成后一次性渲染
代码复杂度高,需处理分片、异常截断极低,两行代码完成
适用场景正式AI对话产品内部简单工具、本地Demo

七、项目扩展优化方向

  1. 增加加载锁:新增loading响应式变量,请求中禁用按钮,防止重复点击
  2. 异常捕获:外层增加try/catch,处理网络失败、401密钥错误、接口限流
  3. Markdown渲染:流式输出纯文本,流结束后引入marked渲染富文本
  4. 多轮对话:扩展messages数组,存储历史聊天上下文,实现连续对话
  5. 中断请求:使用AbortController,支持中途停止AI生成
  6. 换行样式兼容:CSS增加white-space: pre-wrap,保留AI返回换行格式

八、总结

  1. AI产品丝滑打字机交互核心依靠SSE流式输出,原生Fetch+ReadableStream无需第三方SDK即可实现;
  2. buffer缓冲区是流式解析的灵魂,专门解决TCP分包截断JSON的线上致命bug;
  3. DeepSeek接口区分流式/非流式两套返回结构,deltamessage字段切勿混用;
  4. 生产环境优先使用流式输出提升用户体验,同时做好分片容错、异常捕获、密钥安全管理。
相关资讯
点击查看更多
游戏推荐
推荐专题
热门阅读
推荐下载