从 Buffer 到 TextEncoder,从等待焦虑到丝滑如德芙——聊聊前端如何优雅地“接住”AI 的口水话
一、用户不是在等答案,是在等“煎饼果子加几个蛋”
想象一下:你问 AI:“今天适合穿短袖吗?”
结果它沉思三秒,仿佛在推演宇宙命运……
然后,“唰”地一下,整段回答砸你脸上。
这体验,就像你点了个煎饼果子,摊主默默盯着面糊发呆,三分钟后突然把整个摊子塞给你——连锅带铲。
用户要的是“实时感” ,不是“憋大招”。
于是,流式输出(Streaming) 登场了!它让 AI 像真人聊天一样,边想边说,边说边显示。你说一句,它回半句;你刚打完字,它已经开始蹦出第一个字了。
而今天,我们要用 Vue3 + DeepSeek ,配合前端的 Buffer 和 TextEncoder/Decoder,打造一套丝滑到能当洗发水广告的流式交互体验。
二、DeepSeek 说了啥?其实它吐的是“二进制口水”
当你调用 DeepSeek 的流式接口(比如设置 stream: true),后端不会一次性返回完整 JSON,而是通过 HTTP 的 chunked transfer encoding,分块推送数据。
这些数据块长什么样?
不是字符串,不是 JSON 对象,而是 原始的二进制字节流(Uint8Array) 。
是的,AI 的智慧,在网络里就是一串 0 和 1 的排列组合,跟你的微信聊天记录本质上没区别——都是字节。
但浏览器收到的是 Raw Bytes,不能直接塞进 <div> 里显示。怎么办?
这时候,HTML5 的 TextEncoder 和 TextDecoder 就像翻译官登场了:
- TextEncoder:把人类语言(字符串) → 编码成 Uint8Array(给机器看)
- TextDecoder:把 Uint8Array(机器语) → 解码成人类语言(字符串)
但注意!在流式场景中,我们只用 TextDecoder,因为我们要把 AI 吐出来的二进制“口水”翻译成人话。
三、Buffer:前端的“临时口水盆”
在接收流式数据时,数据是分批到达的。可能第一块只有 "你好" 的前两个字节,第二块才补全。
如果直接 decode 每一块,可能会遇到 “截断字符” 的问题(尤其是 UTF-8 中文,一个字占 2~4 字节)。比如:
// 错误示范
const decoder = new TextDecoder();
decoder.decode(new Uint8Array([0xE4, 0xBD])) // ❌ 不完整的 UTF-8,会变成
所以,我们需要一个 缓冲区(Buffer) ,把所有收到的字节先存起来,等凑够一个完整字符再解码。
在浏览器中,我们可以用:
ArrayBuffer+Uint8Array手动管理- 或更聪明的做法:用
TextDecoder的stream模式
const decoder = new TextDecoder('utf-8', { stream: true });
// 每次收到 chunk,直接 decode
let text = decoder.decode(chunk, { stream: true });
// 最后一次调用时不加 stream,表示结束
let final = decoder.decode();
这个 { stream: true } 参数,就是 TextDecoder 的“智能拼图模式”——它会自动缓存不完整的字节,等下一块来了再拼!
这就像你吃火锅,毛肚七上八下,TextDecoder 会帮你数着“还差两下”,绝不让你吃到半生不熟的尴尬。
四、Vue3 如何优雅“接住”AI 的口水?
假设我们用 fetch 调用 DeepSeek 的流式接口:
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: "讲个笑话",
stream: true
})
});
if (!response.body) throw new Error('ReadableStream not supported');
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8', { stream: true });
let fullText = '';
// 在 Vue3 的 setup() 或方法中
const message = ref('');
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码当前 chunk
const chunkText = decoder.decode(value, { stream: true });
fullText += chunkText;
// 响应式更新!Vue3 自动触发 DOM 更新
message.value = fullText;
}
// 最后 flush 剩余缓冲
message.value += decoder.decode();
关键点:
ref响应式变量:每次message.value赋值,Vue3 自动更新视图。TextDecoder({ stream: true }):避免乱码,智能拼接。while + await reader.read():逐块读取,边读边显示。
效果?用户看到文字像打字机一样“哒哒哒”蹦出来,心理等待时间缩短 70% (心理学研究:流动的信息比静止的“加载中”更让人安心)。
五、为什么 Streaming 是用户体验的“作弊器”?
| 场景 | 非流式 | 流式 |
|---|---|---|
| 用户提问 | 等待 3 秒 → 全文出现 | 0.2 秒 → 第一个字出现,持续输出 |
| 心理感受 | “它是不是卡了?” | “哇,它在认真思考!” |
| 可中断性 | 必须等完才能操作 | 看到一半不想听了?直接点停止! |
| 带宽压力 | 一次性传输大 JSON | 小块传输,更省流量 |
流式输出,本质是“时间切片”的艺术——把 AI 的思考过程可视化,让用户参与进来,而不是被动等待。
六、Bonus:别忘了错误处理和 Loading 状态!
现实世界总有网络抖动、AI 抽风。记得加上:
try {
// 上述流式逻辑
} catch (err) {
message.value += `\n[系统错误: ${err.message}]`;
} finally {
isLoading.value = false;
}
还可以加个“打字机音效”(开玩笑的,别真加,用户会骂你)。
七、结语:让 AI 像人一样“说话”
技术的本质,是消除等待的焦虑。
通过 Vue3 的响应式 + DeepSeek 的流式接口 + TextDecoder 的智能解码,我们不仅实现了功能,更重塑了人与 AI 的对话节奏。
下次当你看到屏幕上文字一个字一个字蹦出来时,请记住:
那不是代码在跑,是 AI 在对你眨眼睛。