背景
大家好,我是江辰,最近小小的實現了下 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
,就是我們要給連結傳的引數
問題來了
當我實現之後,發現它在不斷的自動重連?搜了很多文件,想不通,為何會自動重連,這裡伏筆。想不通,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;
}
}
實現沒有問題,在我電腦上也跑通了,能穩定接收服務端訊息,不會自動重連,萬事大吉,轉交朋友試用
。。。。
交給朋友試用,反饋說,會出現回覆不全???,除錯搞起
瀏覽器側接收的訊息
抓包看的訊息
對比看,瀏覽器側丟包!丟包了!!!幾番排查下來,不知為何會丟包,而且是隻有 Windows 上會丟包(必現),macOS 上不會,不懂了呀,我們自己測試 Win 下 ping 都是穩定的,有懂的同學,可以告知下,謝謝!
最終解決方案
又回到 EventSource,沒錯,又回來了,折騰下來發現,每次收完訊息,你必須手動關閉下,evtSource.close();
,才不會自動重連,而且自動重連就是 EventSource 的特性之一,害,伏筆解決了。這個關閉有個前提是,服務端下發欄位告訴你,能關閉,你才能關閉哦,折騰啊!!!
總結
透過這次的學習,讓我對 EventSource 以及 Fetch、Axios 有了一次深刻的認知,大家看完覺得還不錯的話,歡迎點贊,收藏哦
文章同步更新平臺:掘金、CSDN、知乎、思否、部落格,公眾號(野生程式猿江辰)
我的聯絡方式,v:Jiang9684,歡迎和我一起學習交流
完