前言:为什么选择流式响应?
现代AI聊天式交互的核心需求是实时性和渐进呈现。
相比传统接口的"请求-等待-完整响应"模式,流式响应(Streaming Response)能带来更自然的对话体验。
今天咱们就用Vue3+TypeScript手把手实现一个ChatGPT同款效果,让你在10分钟内get核心技术!
一、技术选型:为什么不是原生EventSource?
传统方案痛点:
const source = new EventSource('/api/chat') // 这玩意用过的都懂...
source.onmessage = (e) => { /* 处理消息 */ }
| 能力 | EventSource | fetch-event-source |
|---|---|---|
| 自定义请求头 | ❌ 只能URL传参 | ✅ 支持Authorization等 |
| 请求方法 | 仅GET | ✅ 支持POST/PUT |
| 中断控制 | ❌ 关不掉 | ✅ AbortController掌控全局 |
| 错误重试 | ❌ 固定3秒重试 | ✅ 自定义重试策略 |
| 数据格式 | ❌ 仅文本 | ✅ 支持JSON/二进制 |
小结: 要玩转生产级的流式交互,@microsoft/fetch-event-source才是真香选择!
二、环境准备
npm install @microsoft/fetch-event-source # 这才是流式处理的灵魂
三、核心实现
3.1 类型定义
// types/chat.ts
export interface Message {
id: string // 消息唯一标识
role: 'user' | 'assistant' // 发送方身份
content: string // 消息内容
timestamp: number // 时间戳
}
export type MessageChunk = {
content: string // 当前片段内容
done: boolean // 是否结束标记
}
3.2 流式处理Hook
// hooks/useChatStream.ts
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ref, type Ref } from 'vue'
export const useChatStream = () => {
// 创建中断控制器(用来随时关闭连接)
const controller = new AbortController()
// 响应式状态管理
const isStreaming = ref(false) // 是否正在流式传输
const error = ref<Error | null>(null) // 错误信息
const streamChat = async (messages: Ref<Message[]>, input: string) => {
// 添加用户消息
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: input,
timestamp: Date.now()
}
messages.value = [...messages.value, userMessage]
// 初始化AI消息(内容为空)
const assistantMessage: Message = {
id: `temp_${Date.now()}`,
role: 'assistant',
content: '',
timestamp: Date.now()
}
messages.value = [...messages.value, assistantMessage]
isStreaming.value = true // 开启流式传输
try {
await fetchEventSource('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${localStorage.getItem('token')}` // 携带鉴权信息
},
body: JSON.stringify({ message: input }),
signal: controller.signal, // 绑定中断信号
// 连接建立时的回调
async onopen(response) {
if (!response.ok) throw new Error('服务器开小差了,状态码:'+response.status)
},
// 收到消息时的处理
onmessage(ev) {
const chunk = JSON.parse(ev.data) as MessageChunk
const lastMsg = messages.value[messages.value.length - 1]
// 拼接消息内容(类似打字机效果)
lastMsg.content += chunk.content
// 触发响应式更新(Vue3的魔法就在这里!)
messages.value = [...messages.value.slice(0, -1), lastMsg]
},
// 连接关闭时
onclose() {
// 生成正式ID替换临时ID
const lastMsg = messages.value[messages.value.length - 1]
lastMsg.id = `msg_${Date.now()}`
isStreaming.value = false
},
// 错误处理
onerror(err) {
throw new Error(`流式传输异常:${err}`)
}
})
} catch (err) {
error.value = err as Error
isStreaming.value = false
}
}
return {
streamChat, // 启动流式对话
isStreaming, // 当前状态
error, // 错误对象
cancel: () => controller.abort() // 紧急停止方法
}
}
3.3 聊天组件(视图层)
<!-- components/ChatWindow.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useChatStream } from '@/hooks/useChatStream'
import type { Message } from '@/types/chat'
const messages = ref<Message[]>([]) // 消息列表
const input = ref('') // 输入框内容
const { streamChat, isStreaming, error } = useChatStream()
const handleSubmit = () => {
if (!input.value.trim() || isStreaming.value) return
streamChat(messages, input.value) // 触发流式请求
input.value = '' // 清空输入框
}
</script>
<template>
<div class="chat-container">
<!-- 消息列表 -->
<div v-for="msg in messages"
:key="msg.id"
:class="`message ${msg.role}`">
{{ msg.content }}
</div>
<!-- 输入区域 -->
<form @submit.prevent="handleSubmit">
<input
v-model="input"
:disabled="isStreaming"
placeholder="输入你想问的..."
/>
<button type="submit" :disabled="isStreaming">
{{ isStreaming ? 'AI正在思考...' : '发送' }}
</button>
</form>
<!-- 错误提示 -->
<div v-if="error" class="error">
{{ error.message }}
<button @click="error = null">×</button>
</div>
</div>
</template>
四、错误重试:打造健壮系统
4.1 指数退避重试策略
// 在useChatStream.ts中扩展
const MAX_RETRIES = 3 // 最大重试次数
const BASE_DELAY = 1000 // 基础延迟1秒
const streamChat = async (/* 参数 */) => {
let retries = 0
const retry = () => {
if (retries >= MAX_RETRIES) {
error.value = new Error('服务器连接失败,请稍后再试')
return
}
const delay = BASE_DELAY * Math.pow(2, retries) // 指数退避
retries++
setTimeout(() => {
streamChat(messages, input) // 重新发起请求
}, delay)
}
await fetchEventSource(/* ... */ {
// ...其他配置
onerror(err) {
if (!controller.signal.aborted) {
retry() // 触发重试
}
}
})
}
4.2 断点续传机制
// 在消息处理中添加lastId追踪
let lastEventId = ''
// 请求配置中追加header
headers: {
...,
'Last-Event-ID': lastEventId // 告诉服务端从哪里继续
}
// 在onmessage回调中更新lastId
onmessage(ev) {
if (ev.id) lastEventId = ev.id
// ...其他处理
}
五、效果增强技巧
5.1 丝滑滚动(用户体验++)
<script setup>
// 在ChatWindow.vue中添加
import { nextTick, watch } from 'vue'
const chatContainer = ref<HTMLElement>()
watch(messages, async () => {
await nextTick() // 等待DOM更新
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight
}
}, { deep: true })
</script>
<template>
<div ref="chatContainer" class="chat-container">
<!-- 消息列表 -->
</div>
</template>
5.2 性能优化:消息分块缓冲
// 在useChatStream.ts中
let buffer = '' // 缓冲池
let lastRender = 0
onmessage(ev) {
buffer += ev.data
// 每100ms渲染一次(减少DOM操作)
if (Date.now() - lastRender > 100) {
lastMsg.content += buffer
buffer = ''
lastRender = Date.now()
messages.value = [...messages.value.slice(0, -1), lastMsg]
}
}
onclose() {
// 处理剩余缓冲
if (buffer) {
lastMsg.content += buffer
messages.value = [...messages.value.slice(0, -1), lastMsg]
}
}
六、避坑指南
-
内存泄漏
务必在组件卸载时终止连接:onUnmounted(() => controller.abort()) -
XSS防护
如果渲染HTML内容,记得消毒:import DOMPurify from 'dompurify' lastMsg.content = DOMPurify.sanitize(content) -
流量控制
服务端配置速率限制:// 服务端示例(Node.js) app.post('/api/chat', (req, res) => { res.setHeader('X-RateLimit-Limit', '10') // 每分钟10次 })
总结
现在你已掌握:
✅ 流式交互核心原理
✅ Vue3响应式集成技巧
✅ 生产级错误处理方案
✅ 性能优化实战经验
下一步建议:
- 接入OpenAI API实现真实对话
- 添加Markdown渲染提升可读性
- 实现对话历史持久化存储
# 最后友情提醒:
# 上线前记得配置CORS和HTTPS!
技术雷达:
- 核心库:Vue3.3+ / TypeScript5+
- 流式传输:@microsoft/fetch-event-source
- 样式方案:UnoCSS(推荐)
- 状态管理:Pinia(复杂场景使用)
准备好迎接用户的"Wow Effect"了吗?现在就去打造属于你的智能对话应用吧!
(完) 以上完整代码已经分享到Gitee, 如有所需欢迎自取