ai-hooks 开源地址:github.com/Gijela/ai-h...
你是否也曾想在项目中轻松实现类似 ChatGPT 的流式对话界面,却被繁琐的状态管理、消息拼接、错误处理和生命周期控制所困扰?上期我们精读了 fetch-event-source
库的源码,其虽为我们提供了坚实的底层能力,但直接在业务组件中使用它,就如同给了你一台强大的引擎,却还需要自己动手组装车身、方向盘和仪表盘。
useSse
hook 正是为此而生。它是一把精心打造的"瑞士军刀",专门用于在 Vue3/React 环境下,优雅地处理服务器推送事件 (SSE)。它不仅封装了 fetch-event-source
的所有功能,更在 Vue/React 的响应式系统之上,构建了一套完整的、专为对话式 AI 应用设计的状态管理和交互模型。
本文将以 Vue3 为例,介绍 useSse,你将深入理解:
- 为何需要 useSse :了解它如何从
fetch-event-source
的底层能力中升华,解决在 Vue 应用中的实际痛点。 - "响应式状态机"的设计 :剖析
useSse
如何巧妙地利用ref
将连接状态 (connecting
,streaming
,completed
,error
等) 与 UI 渲染无缝对接。 - 消息流的生命周期管理 :学习
append
,stop
等核心方法如何精巧地控制从用户发送消息到接收、渲染、完成或中断的整个流程。 - 高阶抽象的威力 :通过对比原生
fetch-event-source
的用法,体会useSse
这个高阶 Hook 如何大幅简化代码、提升开发效率和可维护性。 - 最佳实践范例:获得一个即插即用的实战指南,快速在你的项目中集成 AI 对话能力。
准备好用一种更优雅的方式来驾驭 SSE 了吗?让我们开始吧!
缘起:直接用 fetch-event-source
不够香吗?
fetch-event-source
是一个出色的库,它解决了原生 EventSource
的诸多限制。然而,在构建一个功能完备的 Vue 对话界面时,我们面临的挑战远不止建立一个 SSE 连接那么简单:
- 状态管理之痛 :加载状态 (
loading
)、错误信息 (error
)、消息列表 (messages
),这些都需要在 Vue 的响应式系统中进行管理,并与组件的生命周期同步。 - 消息格式化之繁 :从服务器收到的原始数据块 (
data
chunk) 需要被解析、拼接,并转换成结构化的消息对象,才能被 UI 正确渲染。 - 用户交互之杂:需要处理用户输入、发送消息、中断流式响应、重试等多种交互逻辑。
- 生命周期之忧:当组件被卸载时,必须确保 SSE 连接被可靠地关闭,以防止内存泄漏和不必要的后台请求。
直接在组件中处理这些逻辑,会导致代码臃肿、状态分散、难以维护。useSse
的核心使命,就是将这些通用的、复杂的逻辑封装起来,提供一个干净、声明式且高度可复用的接口。
核心思想:一个 Hook,承包整个 AI 对话界面的状态管理
useSse
的设计哲学非常清晰:它不仅仅是一个网络请求的封装,而是一个完整的、面向对话场景的状态管理器。
它将一个典型的 AI 对话流程抽象为以下几个核心要素:
messages
: 一个响应式的消息数组,是整个对话历史的"唯一事实来源"。input
: 一个响应式的字符串,用于双向绑定输入框。loading
: 一个响应式的布尔值,用于控制加载状态的 UI(如禁用发送按钮、显示加载动画)。error
: 一个响应式的错误对象,用于展示连接或解析过程中的异常。append
/stop
/handleSubmit
: 一系列精心设计的方法,用于驱动状态的流转和处理用户交互。
通过将这些状态和方法聚合在一个独立的 Hook 中,我们将复杂的业务逻辑从视图组件中剥离,使得组件只需关注如何渲染这些状态,从而实现了"逻辑"与"视图"的完美分离。
架构之美:响应式状态机与生命周期管理
useSse
的内部实现,可以看作一个围绕 SSE 连接生命周期构建的"响应式状态机"。其状态转换的核心,体现在 AssistantSseMessage
的 status
字段上。
status
(定义于 SseStatusEnum
) 是驱动 UI 动态变化的关键。它精确地描述了助理消息从诞生到终结的每一个阶段:
idle
: 初始状态。connecting
: 连接已发起,等待服务器响应。streaming-content
: 正在接收并渲染内容。completed
: 消息流正常结束。aborted
: 被用户手动中断。error
: 发生错误。
useSse
内部的 _sseFetch
函数通过监听 fetch-event-source
的 onopen
, onmessage
, onclose
, onerror
等事件,在关键节点更新最后一条助理消息的 status
,从而自动触发 Vue 组件的重新渲染。
此外,通过 onUnmounted
生命周期钩子,useSse
确保了当使用它的组件被销毁时,会自动调用 stop()
方法来中止任何正在进行的连接,有效防止了资源泄漏。
API 详解
useSse
的 API 设计简洁而强大,分为"配置选项"和"返回值"两部分。
useSse(options: UseSseOptions)
初始化 Hook 时,你需要传入一个配置对象。
参数 | 类型 | 必需 | 描述 |
---|---|---|---|
initialInput |
string |
否 | 输入框的初始值。 |
initialMessages |
SseMessage[] |
否 | 初始的消息列表。 |
formatMessage |
(data, message) => AssistantSseMessage |
是 | 核心函数 。用于将 SSE onmessage 事件中的原始 data 字符串,解析并合并到上一条助理消息中,返回更新后的消息对象。 |
onOpen |
(response: Response) => void |
否 | 连接成功建立时的回调,可用于检查响应头。 |
onOpenJson |
(json: any) => void |
否 | 当服务器返回非流式 JSON 响应(通常是错误信息)时的回调。 |
onMessage |
(data: string) => void |
否 | 每次收到原始 data 字符串时的回调。 |
onFinish |
(message: SseMessage) => void |
否 | 消息流正常结束 ([DONE] ) 时的回调。 |
onClose |
() => void |
否 | 连接关闭时的回调(无论正常或异常)。 |
onError |
(error: any) => void |
否 | 发生任何错误时的回调。 |
返回值 (Returned Values)
调用 useSse
会返回一个包含响应式状态和方法的对象。
名称 | 类型 | 描述 |
---|---|---|
messages |
Ref<SseMessage[]> |
响应式的消息数组,可以直接在模板中 v-for 渲染。 |
error |
Ref<any> |
响应式的错误对象。 |
loading |
Ref<boolean> |
响应式的加载状态。 |
input |
Ref<string> |
响应式的输入框内容,可使用 v-model 进行绑定。 |
append |
(prompt, options) => Promise<void> |
核心方法。追加一条用户消息和一条空的助理消息,并立即开始 SSE 请求。 |
stop |
() => void |
核心方法。手动中止当前的 SSE 连接。 |
setMessages |
(messages: SseMessage[]) => void |
手动设置消息列表。 |
handleInputChange |
(e: Event) => void |
用于原生 <input> 的 onchange 事件处理器,与 input Ref 配合使用。 |
handleSubmit |
(e: Event, options) => void |
便捷的表单提交处理器。它会阻止默认事件,调用 append ,并清空输入框。 |
快速上手:构建一个简单的 AI 聊天机器人
下面是一个在 Vue 3 <script setup>
组件中使用 useSse
的完整示例。
vue
<script setup lang="ts">
import { useSse } from '@/hooks/useSse'
import type { AssistantSseMessage } from '@/hooks/useSse/types'
// 1. 初始化 useSse Hook
const { messages, input, loading, handleSubmit, stop, handleInputChange } = useSse({
// 2. 提供核心的消息格式化逻辑
formatMessage: (data, lastMessage) => {
try {
// 假设服务器返回的是 JSON 字符串
const jsonData = JSON.parse(data)
// 将新的数据块追加到上一条消息的内容上
const updatedContent = lastMessage.content + (jsonData.content || '')
return {
...lastMessage,
content: updatedContent,
status: 'streaming-content', // 更新状态
}
} catch (e) {
console.error('JSON parse error:', e)
return {
...lastMessage,
status: 'error',
}
}
},
// 可选:添加其他回调
onError: (err) => {
console.error('SSE Error:', err)
},
})
// 3. 定义提交时需要传递给后端的参数
const getSseOptions = () => ({
url: '/api/v1/chat/completions',
method: 'POST',
body: {
// 假设你的 API 需要历史消息
messages: messages.value.map(({ role, content }) => ({ role, content })),
stream: true,
},
})
</script>
<template>
<div class="chat-container">
<div class="message-list">
<div v-for="msg in messages" :key="msg.id" class="message" :class="`message--${msg.role}`">
<p>{{ msg.content }}</p>
<!-- 根据 status 显示不同的 UI -->
<div
v-if="
msg.role === 'assistant' && (msg as AssistantSseMessage).status === 'streaming-content'
"
class="typing-indicator"
></div>
</div>
</div>
<!-- 用户可以随时中止 -->
<button v-if="loading" @click="stop">Stop</button>
<form @submit="(e) => handleSubmit(e, getSseOptions())">
<input
:value="input"
@input="handleInputChange"
placeholder="Type your message..."
:disabled="loading"
/>
<button type="submit" :disabled="loading">Send</button>
</form>
</div>
</template>
<style scoped>
/* ... 添加一些美化样式 ... */
.message {
padding: 8px;
margin-bottom: 8px;
border-radius: 4px;
}
.message--user {
background-color: #e1f5fe;
text-align: right;
}
.message--assistant {
background-color: #f1f1f1;
}
.typing-indicator {
width: 8px;
height: 8px;
background-color: #333;
border-radius: 50%;
animation: typing 1s infinite;
}
@keyframes typing {
0% {
opacity: 0.2;
}
50% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
</style>
流程图示
下面是调用 append
方法后,useSse
内部的典型工作流程图,帮助你更直观地理解其运行机制。
通过这个精心设计的 useSse
Hook,我们可以用一种极其声明式和可维护的方式,在 Vue 应用中构建出功能强大、体验流畅的 AI 对话产品。