useSse 开源:如何把流式数据请求/处理简化到极致

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 连接那么简单:

  1. 状态管理之痛 :加载状态 (loading)、错误信息 (error)、消息列表 (messages),这些都需要在 Vue 的响应式系统中进行管理,并与组件的生命周期同步。
  2. 消息格式化之繁 :从服务器收到的原始数据块 (data chunk) 需要被解析、拼接,并转换成结构化的消息对象,才能被 UI 正确渲染。
  3. 用户交互之杂:需要处理用户输入、发送消息、中断流式响应、重试等多种交互逻辑。
  4. 生命周期之忧:当组件被卸载时,必须确保 SSE 连接被可靠地关闭,以防止内存泄漏和不必要的后台请求。

直接在组件中处理这些逻辑,会导致代码臃肿、状态分散、难以维护。useSse 的核心使命,就是将这些通用的、复杂的逻辑封装起来,提供一个干净、声明式且高度可复用的接口。

核心思想:一个 Hook,承包整个 AI 对话界面的状态管理

useSse 的设计哲学非常清晰:它不仅仅是一个网络请求的封装,而是一个完整的、面向对话场景的状态管理器。

它将一个典型的 AI 对话流程抽象为以下几个核心要素:

  • messages: 一个响应式的消息数组,是整个对话历史的"唯一事实来源"。
  • input: 一个响应式的字符串,用于双向绑定输入框。
  • loading: 一个响应式的布尔值,用于控制加载状态的 UI(如禁用发送按钮、显示加载动画)。
  • error: 一个响应式的错误对象,用于展示连接或解析过程中的异常。
  • append / stop / handleSubmit: 一系列精心设计的方法,用于驱动状态的流转和处理用户交互。

通过将这些状态和方法聚合在一个独立的 Hook 中,我们将复杂的业务逻辑从视图组件中剥离,使得组件只需关注如何渲染这些状态,从而实现了"逻辑"与"视图"的完美分离。

架构之美:响应式状态机与生命周期管理

useSse 的内部实现,可以看作一个围绕 SSE 连接生命周期构建的"响应式状态机"。其状态转换的核心,体现在 AssistantSseMessagestatus 字段上。

status (定义于 SseStatusEnum) 是驱动 UI 动态变化的关键。它精确地描述了助理消息从诞生到终结的每一个阶段:

  • idle: 初始状态。
  • connecting: 连接已发起,等待服务器响应。
  • streaming-content: 正在接收并渲染内容。
  • completed: 消息流正常结束。
  • aborted: 被用户手动中断。
  • error: 发生错误。

useSse 内部的 _sseFetch 函数通过监听 fetch-event-sourceonopen, 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 内部的典型工作流程图,帮助你更直观地理解其运行机制。

sequenceDiagram participant Component as Vue组件 participant UseSse as useSse Hook participant FetchEventSource as fetch-event-source participant Server as 服务器 Component->>UseSse: 调用 append(prompt, sseOptions) activate UseSse UseSse->>UseSse: 更新 messages (添加用户和空的助理消息) UseSse->>UseSse: 设置 loading.value = true Note right of UseSse: 触发UI更新 UseSse->>FetchEventSource: 调用 fetchEventSource(url, options) activate FetchEventSource FetchEventSource->>Server: 发起 HTTP 请求 Server-->>FetchEventSource: 响应流 (Response Stream) FetchEventSource->>UseSse: onopen 回调 UseSse->>UseSse: 更新最后一条消息 status = 'connecting' Note right of UseSse: 触发UI更新 loop 接收数据流 Server-->>FetchEventSource: 推送数据块 (chunk) FetchEventSource->>UseSse: onmessage 回调 (携带 data) UseSse->>UseSse: 调用 formatMessage(data, lastMessage) UseSse->>UseSse: 更新 messages (合并内容并更新 status) Note right of UseSse: 实时打字机效果 end alt 正常结束 Server-->>FetchEventSource: 推送 [DONE] FetchEventSource->>UseSse: onmessage 回调 UseSse->>UseSse: 更新最后一条消息 status = 'completed' UseSse->>UseSse: 调用 onFinish 回调 else 异常/中断 alt 用户中断 Component->>UseSse: 调用 stop() UseSse->>FetchEventSource: controller.abort() FetchEventSource->>UseSse: onerror/onclose 回调 UseSse->>UseSse: 更新最后一条消息 status = 'aborted' end alt 服务器/网络错误 FetchEventSource->>UseSse: onerror 回调 UseSse->>UseSse: 更新 error.value UseSse->>UseSse: 更新最后一条消息 status = 'error' end end UseSse->>UseSse: 设置 loading.value = false deactivate FetchEventSource deactivate UseSse Note over Component: UI 最终状态渲染

通过这个精心设计的 useSse Hook,我们可以用一种极其声明式和可维护的方式,在 Vue 应用中构建出功能强大、体验流畅的 AI 对话产品。

相关推荐
江城开朗的豌豆3 分钟前
小程序登录不迷路:一篇文章搞定用户身份验证
前端·javascript·微信小程序
aesthetician7 分钟前
React 19.2.0: 新特性与优化深度解析
前端·javascript·react.js
FIN666822 分钟前
射频技术领域的领航者,昂瑞微IPO即将上会审议
前端·人工智能·前端框架·信息与通信
U.2 SSD32 分钟前
ECharts漏斗图示例
前端·javascript·echarts
江城开朗的豌豆32 分钟前
我的小程序登录优化记:从短信验证到“一键获取”手机号
前端·javascript·微信小程序
excel35 分钟前
Vue Mixin 全解析:概念、使用与源码
前端·javascript·vue.js
IT_陈寒42 分钟前
Java性能优化:这5个Spring Boot隐藏技巧让你的应用提速40%
前端·人工智能·后端
勇往直前plus1 小时前
CentOS 7 环境下 RabbitMQ 的部署与 Web 管理界面基本使用指南
前端·docker·centos·rabbitmq
北海-cherish7 小时前
vue中的 watchEffect、watchAsyncEffect、watchPostEffect的区别
前端·javascript·vue.js
2501_915909068 小时前
HTML5 与 HTTPS,页面能力、必要性、常见问题与实战排查
前端·ios·小程序·https·uni-app·iphone·html5