fetchEventSource使用+源码解析
fetchEventSource使用+源码解析
前言
最近由于一些乱七八糟的原因,接触到了国内开发的一些类ChatGPT的API的前端调用与功能集成。概括的来说,就是有一个需求,需要在Web前端页面中集成类似于AI聊天助手的功能,而怎么去跟这个AI助手一起聊天呢?这时候就需要调这个GPT助手的API来实现了。
不过我们都知道,在平常和ChatGPT一起聊天的时候,你问他问题,他不是一下子全部加载好回答出来的,他是会将所有的返回信息处理成一个 信息流 的方式进行返回,所以我们是可以看到ChatGPT在回答消息的时候是逐字逐行来输出的。而浏览器本身也是通过EventSource这一个内置的API来接收这 流式SSE 的数据并处理。
不过这个EventSource有一个非常致命的缺点,那就是 只支持GET类型的请求,并且不支持任何自定义的头部 。这也就意味着,你如果想要和ChatGPT双向聊天,你发给他消息,他以信息流的方式返回给你数据,你再在这个消息的基础之上再发给它消息,这时使用EventSource就是行不通的。
而这个时候就得使用我们今天的主角了,微软开发的一个专门用于处理双向SSE数据流的npm库:fetch-event-source。这个包的主要作用是提供一个遵循 WHATWG Fetch 标准的 API 来处理 SSE,不但允许我们可以和对应的url地址简历持久连接,并且允许我们在接收数据流信息的同时将我们想要发送的消息也通过相同的url进行发送。实际上目前的ChatGPT实现的双向信息流也是基于这个库进行开发的。
使用方法
先来讲一讲这个库的一些基本使用方法(主要是举我自己的使用例子)。
安装
输入以下的命令即可安装:
npm install --save @microsoft/fetch-event-source
使用步骤
其实fetch-event-source的使用非常非常的简单纯粹,不过前提是你得有那种使用的场景。使用 @microsoft/fetch-event-source
的步骤可以这样拆分:
- 导入模块:在编写TS/JS代码的部分导入
fetchEventSource
函数,我是在Vue3中的TS环境使用的。 - 配置请求:配置你的请求,包括指定要连接的服务器端点 URL 和其他任何需要的 HTTP 请求头或设置。
- 处理事件:使用
fetchEventSource
发起请求并处理不同的事件。你可以定义onmessage
、onopen
、onerror
和onclose
回调来处理相应的事件。 - 处理消息:在
onmessage
回调中,你会接收到服务器发送的所有消息。这些消息通过event.data
访问。你可以根据应用的需求来处理这些消息。比如目前的返回数据是以流式的形式一点点逐渐接收的,那么这个onmessage
回调就会 一直被不停的触发 ,每一次触发都会调用其内部的逻辑。 - 错误处理:在
onerror
回调中,你可以处理任何在连接或接收数据过程中发生的错误,这种错误也是由与你建立连接的服务端进行定义并发送。接收到服务端传来的错误后,会触发这个回调进行自定义的错误处理。 一般地,如果有引入signal作为断开连接的机制,在这个回调中会使用signal.abort()
方法来终止连接。 - 关闭连接:一般当一次流式数据发送完毕后,与你建立连接的url会主动的断开SSE,这时候标志着一次会话发送完毕,你可以在这个回调里面处理一些完成会话的操作。如果你需要主动关闭连接,可以在适当的时候调用由
fetchEventSource
返回的close
方法。
实际例子
我先举一个我在使用的例子,目前我有一个类GPT的人工智能流式对话接口url,即下方的 /api/chat/sseResponse
,我可以将我自己的消息发给它,然后来接收它返回的数据流,组装拼接成一条完整的消息之后插到整个消息列表中作为一个item。
以下是提交对话的具体函数,环境为 Vue3
当中的 <script setup lang="ts">
脚本当中:
// 提交对话
const submitChat = async () => {
if (loading.value) return
if (!inputContent.value) {
ElMessage.warning('请输入内容')
return
}
loading.value = true
const chatItem: ChatMessage = {
id: String(chatsList.value.length + 1),
content: inputContent.value,
isMe: true
}
chatsList.value.push(chatItem)
inputContent.value = ''
const ctrl = new AbortController() // 创建AbortController实例,以便中止请求
await fetchEventSource('/api/chat/sseResponse', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(chatMessageList.value),
openWhenHidden: true, // 取消visibilityChange事件
signal: ctrl.signal, // AbortSignal
async onmessage(ev) {
const data = JSON.parse(ev.data)
if (data.sceneUuid !== sceneUuid.value) sceneUuid.value = data.sceneUuid
if (data.token !== '') answer.value += data.token
await nextTick()
if (main.value) {
const { scrollHeight } = main.value
main.value.scrollTop = scrollHeight
}
},
onclose() {
if (answer.value) {
const chatItem: ChatMessage = {
id: String(chatsList.value.length + 1),
content: answer.value,
isMe: false
}
chatsList.value.push(chatItem)
answer.value = ''
}
loading.value = false
},