EventSource 引發的一系列事件

江辰發表於2023-04-29

背景

大家好,我是江辰,最近小小的實現了下 chatGPT 的問答式回覆,調研了前端如何實現這種問答式請求,有幾種方案,Http、EventSource、WebSocket,三種實現方案各有優缺點,Http 和 WebSocket ,想必大家耳聞能詳,這裡我講講 EventSource

EventSource

EventSource 是伺服器推送的一個網路事件介面。一個 EventSource 例項會對 HTTP 服務開啟一個持久化的連線,以text/event-stream 格式傳送事件,會一直保持開啟直到被要求關閉。
一旦連線開啟,來自服務端傳入的訊息會以事件的形式分發至你程式碼中。如果接收訊息中有一個事件欄位,觸發的事件與事件欄位的值相同。如果沒有事件欄位存在,則將觸發通用事件。
WebSockets,不同的是,服務端推送是單向的。資料資訊被單向從服務端到客戶端分發。當不需要以訊息形式將資料從客戶端傳送到伺服器時,這使它們成為絕佳的選擇。例如,對於處理社交媒體狀態更新,新聞提要或將資料傳遞到客戶端儲存機制(如 IndexedDB 或 Web 儲存)之類的,EventSource 無疑是一個有效方案。

--- 引自 MDN

對比 WebSocket,它就是簡單,方便,在特定的一些場景下,比如聊天訊息或市場價格,這就是 EventSource 擅長的

使用方式

它的使用方式極其簡單

const evtSource = new EventSource('sse.php');
const eventList = document.querySelector('ul');

evtSource.onmessage = function(e) {
 let newElement = document.createElement("li");

  newElement.textContent = "message: " + e.data;
  eventList.appendChild(newElement);
}

對吧,幾行程式碼搞定,如何攜帶引數,在 new EventSource('sse.php?id=123'); 其中 id=123,就是我們要給連結傳的引數

問題來了

image

當我實現之後,發現它在不斷的自動重連?搜了很多文件,想不通,為何會自動重連,這裡伏筆。想不通,ok,我就換個思路,改用 Axios 實現

axios

axios 實現如下

const streamToString = async (readableStream) => {
  return new Promise((resolve, reject) => {
    const chunks = [];
    readableStream.on("data", (data) => {
      chunks.push(data);
    });
    readableStream.on("end", () => {
      resolve(Buffer.concat(chunks).toString('base64'))
    });
    readableStream.on("error", reject);
  });
}


axios({
  method: 'get',
  url:`//xxx/api/chat/stream?prompt=${textarea.current.value.trim()}`,
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  responseType: 'stream'
}).then(async res => {
  const raw = await streamToString(res.data);
})

此時還不知問題的嚴重性!實現完之後,發現不對勁啊,readableStream.on is not a fucntion,???(黑人問號臉),遂列印 log 看看輸出的 res.data 是啥,字串?根本不是一個方法啊,但看網上實現,是這樣啊,沒錯?又看了幾遍,都是這樣實現的,很懵,直到看了下 axios 的 issue,傳送門,2016年就有人提出了這個問題,也就是說 axios 在瀏覽器側一直沒有實現 steram,我內心cnm,網上的文件都是假的!!!

也就是說,按照目前MDN說法,responseType 支援的型別有,arraybuffer、blob、document、json、text、ms-stream,其中 ms-stream,此響應型別僅允許用於下載請求,並且僅受 Internet Explorer 支援

坑坑坑,又要開始了其他方案,想想 Fetch 能不能行,瀏覽器原生支援哦!

Fetch

Fetch API 提供了一個 JavaScript 介面,用於訪問和操縱 HTTP 管道的一些具體部分,例如請求和響應。它還提供了一個全域性 fetch() 方法,該方法提供了一種簡單,合理的方式來跨網路非同步獲取資源。
這種功能以前是使用 XMLHttpRequest 實現的。Fetch 提供了一個更理想的替代方案,可以很容易地被其他技術使用,例如 Service Workers。Fetch 還提供了專門的邏輯空間來定義其他與 HTTP 相關的概念,例如 CORS 和 HTTP 的擴充套件。

--- 引自 MDN

利用 Fetch 實現瞭如下程式碼

const response = await fetch(`//xxx/api/chat/stream?prompt=${textarea.current.value.trim()}`);
const reader = response.body.getReader();

const eventList = document.querySelector('ul');
while (true) {
  const { value, done } = await reader.read();
  const utf8Decoder = new TextDecoder('utf-8');
  let data: any = value ? utf8Decoder.decode(value, {stream: true}) : '';
  try {
    data = JSON.parse(data)
    if (data.id || !data.content) {
      return
    }

    let newElement = document.createElement("li");
    newElement.textContent = "message: " + data.content;
    eventList.appendChild(newElement);
  } catch (e) {
  }
  if (done) {
    break;
  }
}

實現沒有問題,在我電腦上也跑通了,能穩定接收服務端訊息,不會自動重連,萬事大吉,轉交朋友試用
。。。。

交給朋友試用,反饋說,會出現回覆不全???,除錯搞起

瀏覽器側接收的訊息
image

抓包看的訊息
image

對比看,瀏覽器側丟包!丟包了!!!幾番排查下來,不知為何會丟包,而且是隻有 Windows 上會丟包(必現),macOS 上不會,不懂了呀,我們自己測試 Win 下 ping 都是穩定的,有懂的同學,可以告知下,謝謝!

最終解決方案

又回到 EventSource,沒錯,又回來了,折騰下來發現,每次收完訊息,你必須手動關閉下,evtSource.close();,才不會自動重連,而且自動重連就是 EventSource 的特性之一,害,伏筆解決了。這個關閉有個前提是,服務端下發欄位告訴你,能關閉,你才能關閉哦,折騰啊!!!

總結

透過這次的學習,讓我對 EventSource 以及 Fetch、Axios 有了一次深刻的認知,大家看完覺得還不錯的話,歡迎點贊,收藏哦
文章同步更新平臺:掘金、CSDN、知乎、思否、部落格,公眾號(野生程式猿江辰)

我的聯絡方式,v:Jiang9684,歡迎和我一起學習交流

相關文章