GPT打字机效果—— fetchEventSouce进行sse流式请求
需求背景
在GPT爆发的时候,各项目都想给自己的产品加上AI,蹭上AI的风口,因此在最近的一个需求,就想要给项目加入Ai的功能,原本要求的效果是,查询到对应的数据后,完全展示出来,也就是常规的post请求,后来这种效果遇到了一个很现实的问题:长时间的等待。我们需要在GPT返回全部数据后,前端才能接受并展示,一旦询问的时间过长,就会让用户等待很久,这时候我们需要将前端的展示效果改为想ChatGPT那样的打字机效果。
预计的效果如下图:
实现
像这种效果我们很容易就能想到,前端与后端是需要建立连接的,一般前后端建立连接我们第一时间想到的是利用websocket建立通信。但是websocket是双向的,不仅前端需要接受信息,后端也需要接受信息,但是像GPT我们进行询问时,其实只需要前端实时接受信息即可,后端是不需要实时的接受前端的信息。因此我们使用比websocket更加轻量的通信协议:EventStream
EventStream基本用法
与 WebSocket 不同的是,服务器发送事件是单向的。数据消息只能从服务端到发送到客户端(如用户的浏览器)。这使其成为不需要从客户端往服务器发送消息的情况下的最佳选择。
const evtSource = new EventSource("/api/v1/sse")
// 每次连接开启时调用
evtSource.onopen = function () {
console.log("连接开始启动");
};
// 每次接受数据时调用
evtSource.onmessage = (e) => {
console.log('输入每次接受的数据',e)
};
// 每次连接发生错误时调用
evtSource.onerror = function () {
console.log("连接发生错误");
};
需要注意的是,EventSource是以get方式发送请求,对于post请求原生的EventSource是无法实现的
如何用post的方式进行eventSource请求
常见的是通过@microsoft/fetch-event-source 这个库里的fetchEventSource来实现
import { fetchEventSource } from '@microsoft/fetch-event-source';
这个库封装了一个方法,使得我们可以便捷的通过这个方法直接进行调用
以下是具体的代码
const [controller, setController] = useState<any>(new AbortController());
const url = 'http:xxx';
fetchEventSource(url, {
method: 'POST',
headers: {
// SYSTEM_PORTAL_TYPE: 'LINGXI_RUNNING',
'Content-Type': 'text/event-stream',
'X-CSRF-TOKEN': '1232123',
// Cookies: 'ZSMART_LOCALE=zh; ',
},
mode: 'cors',
openWhenHidden: true,
credentials: 'include',
signal: controller?.signal,
onmessage: async (event: any) => {
console.log('eventeventeventeventeventevent');
console.log(event);
},
onerror(err: any) {
console.log('err', err);
},
async onopen(response: any) {
if (response.ok) {
console.log('开始建立连接');
}
},
onclose() {
console.log('关闭');
controller?.abort();
setController(new AbortController());
throw new Error();
},
}).catch((err: any) => {
controller?.abort();
setController(new AbortController());
console.log({ err });
throw new Error(err);
});
值得注意的是,在使用fetchEventSource遇到了这么几个问题,分享出来大家踩踩坑
- 框架内部代理无法使用。若使用了自身的框架代理(这里我用的是umi),若没做特殊处理并不会走事件流的形式,而是在数据统一接受完成后一次性返回。因此这里我们直接写入http形式的请求地址
- 不同源时cookie无法携带。因为使用了http形式而不是代理,这就导致了本机调试时是无法携带cookie到服务端,在一些cookie鉴权的场景会导致鉴权失败。这是浏览器的安全策略,这里我们利用谷歌的插件进行非同源的cookie传送,具体插件百度一下就有