本文最后更新于:2026-08-26T16:33:04+08:00
前端的“流式输出”本质是:后端不要等 AI 完整生成后再返回,而是边生成边写入 HTTP 响应;
Vue3 通过 ReadableStream 持续读取数据,并把新文本追加到响应变量中。
1 2 3 4 5 6 7
| Vue3 │ fetch + ReadableStream ▼ 后端接口 │ 持续请求 AI 服务 ▼ AI 模型
|
后端必须设置接口响应头是:
1 2 3
| Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive
|
| 响应头 |
值 |
作用 |
| Content-Type |
text/event-stream |
声明数据格式为事件流,触发浏览器流式解析。 |
| Cache-Control |
no-cache |
禁止缓存。防止浏览器或代理服务器缓存响应内容,确保每次收到的都是最新实时数据。 |
| Connection |
keep-alive |
保持连接。明确告知客户端和中间网络设备不要关闭 TCP 连接,允许在同一连接上持续传输数据。 |
接口返回SSE格式:
1 2 3 4 5 6 7
| data: {"content":"你好",format:"text"}
data: {"content":",这是",format:"markdown"}
data: {"content":"流式输出"}
data: [DONE]
|
vue3前端封装实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
| import {ref} from 'vue'
export function useChatStream() { const content = ref('') const loading = ref(false) const error = ref('')
let controller: AbortController | null = null
async function sendMessage(message: string) { content.value = '' error.value = '' loading.value = true controller = new AbortController()
try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({message}), signal: controller.signal })
if (!response.ok) { throw new Error(`请求失败:${response.status}`) }
if (!response.body) { throw new Error('浏览器不支持流式响应') }
const reader = response.body.getReader() const decoder = new TextDecoder('utf-8') let buffer = ''
while (true) { const {value, done} = await reader.read()
if (done) break
buffer += decoder.decode(value, {stream: true})
const events = buffer.split('\n\n') buffer = events.pop() || ''
for (const event of events) { const line = event .split('\n') .find(line => line.startsWith('data:'))
if (!line) continue
const data = line.slice(5).trim()
if (data === '[DONE]') { return }
try { const parsed = JSON.parse(data) content.value += parsed.content || '' } catch { content.value += data } } } } catch (err: any) { if (err.name !== 'AbortError') { error.value = err.message || '请求失败' } } finally { loading.value = false controller = null } }
function stop() { controller?.abort() loading.value = false }
return { content, loading, error, sendMessage, stop } }
|
在组件中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| <script setup lang="ts"> import { ref } from 'vue' import { useChatStream } from '@/composables/useChatStream'
// 输入框内容也使用 ref 保存,v-model 会负责双向同步 const input = ref('')
// 解构出组合式函数提供的响应式状态和操作方法 const { content, loading, error, sendMessage, stop } = useChatStream()
function submit() { // trim() 过滤空白输入;加载中时禁止重复发送 if (!input.value.trim() || loading.value) return
// 发送当前输入内容;流式结果会不断写入 content sendMessage(input.value) } </script>
<template> <div class="chat"> <div class="response"> <!-- content 每次追加片段时,Vue 会自动重新渲染这里 --> {{ content }} <!-- 生成期间显示光标,让用户知道内容仍在输出 --> <span v-if="loading">▌</span> </div>
<!-- 只有发生错误时才显示错误信息 --> <p v-if="error" class="error"> {{ error }} </p>
<!-- v-model 将 textarea 的值同步到 input --> <textarea v-model="input" />
<!-- 根据 loading 在“发送”和“停止生成”按钮之间切换 --> <button v-if="!loading" @click="submit"> 发送 </button>
<button v-else @click="stop"> 停止生成 </button> </div> </template>
|
也可以用vue3的插件@ai-sdk/vue
npm i @ai-sdk/vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <script setup lang="ts"> import { useChat } from '@ai-sdk/vue'
const { messages, sendMessage, status, stop } = useChat({ api: '/api/chat' })
function submit() { sendMessage({ text: '请介绍一下 Vue 3' }) } </script>
<template> <div v-for="message in messages" :key="message.id"> <strong>{{ message.role }}:</strong> <span>{{ message.content }}</span> </div>
<button v-if="status === 'streaming'" @click="stop" > 停止 </button>
<button v-else @click="submit" > 发送 </button> </template>
|
vue3实现流式输出
https://zouhualu.github.io/20251121/vue3实现流式输出/