GPT打字机效果—— fetchEventSouce进行sse流式请求

发布时间 2024-01-10 16:07:56作者: 迷失哥哥

需求背景

在GPT爆发的时候,各项目都想给自己的产品加上AI,蹭上AI的风口,因此在最近的一个需求,就想要给项目加入Ai的功能,原本要求的效果是,查询到对应的数据后,完全展示出来,也就是常规的post请求,后来这种效果遇到了一个很现实的问题:长时间的等待。我们需要在GPT返回全部数据后,前端才能接受并展示,一旦询问的时间过长,就会让用户等待很久,这时候我们需要将前端的展示效果改为想ChatGPT那样的打字机效果。
预计的效果如下图:
image

实现

像这种效果我们很容易就能想到,前端与后端是需要建立连接的,一般前后端建立连接我们第一时间想到的是利用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遇到了这么几个问题,分享出来大家踩踩坑

  1. 框架内部代理无法使用。若使用了自身的框架代理(这里我用的是umi),若没做特殊处理并不会走事件流的形式,而是在数据统一接受完成后一次性返回。因此这里我们直接写入http形式的请求地址
  2. 不同源时cookie无法携带。因为使用了http形式而不是代理,这就导致了本机调试时是无法携带cookie到服务端,在一些cookie鉴权的场景会导致鉴权失败。这是浏览器的安全策略,这里我们利用谷歌的插件进行非同源的cookie传送,具体插件百度一下就有