前言
首先聽到服務端訊息推送一般大多數可能都是使用到的websocket
,但是websocket
一般主要是用於聊天室,而SSE(Server-Sent Events)
也是可以基於伺服器來與Web頁面來進行資料和訊息的推送的,但是與websocket
不同的是,伺服器傳送事件是單項的,資料只能從服務端傳送到客戶端。例如我們可以看一下常用的chatGtp
他的文字輸出就是基於伺服器的訊息推送進行輸出的。下面我們見看如何來建立一個通訊連線
EventSource
EventSource
是一個例項,專門用來建立與伺服器的連線接收伺服器的訊息推送的,他會與伺服器建立一個HTTP
的長連線,他會一致保持開啟,直到呼叫close
關閉連線。ps:他是無法使用axios
的因為axios
內部其實使用的是XMLHttpRequest,他是無法支援接收服務端推送的訊息的。
我們先實現一個簡單的後臺服務,然後前臺來使用這個例項看一下效果是怎麼樣的。
const article = `警告:當不使用 HTTP/2 時.....。` app.get('/chat_typing', (req, res) => { // 開啟 Server-sent events res.setHeader('Content-Type', 'text/event-stream') let index = 0 let timerId = 0 // 模擬每隔 0.1s 向前端推送一次 timerId = setInterval(() => { // 獲取文字 const data = article[index] // 下標累加 index++ // 響應結果 if (data) { // data:表示資料內容,\n\n 表示結尾。 res.write(`data: ${data}\n\n`) } else { res.end() clearInterval(timerId) } }, 100) })
這裏我們簡單實現了一個node服務,主要的就是Content-Type
這裏我們把他設定爲了text/event-stream
,也就是服務端的訊息推送,然後我們每100毫秒推送一個字元過去,然後我們再前臺看一下接收的效果是什麼樣子的
<script setup> import { ref } from 'vue' const article = ref('') let source const OpenSSE = () => { source = new EventSource('http://localhost:3000/chat_typing') // 接收資訊 source.addEventListener('message', (e) => { // 實時輸出字串 article.value += e.data }) } </script> <template> <div> <div> <button @click="OpenSSE">開啟SSE</button> </div> <div>{{ article }}</div> </div> </template>
前臺內我們就是宣告了一個EventSource
例項,然後點選開啟的時候監聽他的message
,拿到伺服器推送過來的訊息,進行組裝展示。就實現瞭如下的效果
這裏我們可以看到他確實是開啟了一個長連線,一直在接收伺服器推送過來的資料,且請求型別也變成了eventsource
,我們在某些情況下需要關閉這個連線只需要呼叫例項方法內的close
方法即可對其進行關閉
const CloseSSE = () => { source.close() }
最佳化連線
EventSource
雖然可以讓我們實現一些簡單場景下的伺服器訊息推送的接收,但是他只支援get
請求,並且只能透過路徑拼接的方式進行引數的攜帶。但是我們如果遇到像chat
一樣會攜帶上下文進行數據傳輸的時候那麼就不太適合了,因為處理起來比較麻煩,並且有著一定長度的限制。那麼我們就可以使用fetch
來進行連線,下面我們看一下如何使用fetch
來建立連線獲取伺服器推送過來的訊息。
const OpenSSE = async () => { const res = await fetch('http://localhost:3000/chat_typing') console.log(res) }
這裏我們呢就實現了連線,那麼如何去停呢,我們需要使用到 AbortController
來終止請求
const OpenSSE = async () => { abort = new AbortController() const res = await fetch('http://localhost:3000/chat_typing', { signal: abort.signal, }) console.log(res) } const CloseSSE = () => { abort.abort() }
abort
可以用來終止一個未完成的非同步操作,可以用來終止所有的響應和流。正好我們現在使用fetch
返回過來的就是一個流,那麼解決了請求和終止操作,接下來就需要解析返回的流了
解析流
我們首先列印res.body
可以看到內部存在一個方法,getReader,他呢就是用來獲取流資料的,我們是用res.body.getReader
可以獲取到一個一個Promise
,那麼我們就可以進行執行拿到Promise
的值。
const res = await fetch('http://localhost:3000/chat_typing', { signal: abort.signal, }) const content = res.body.getReader() const decode = new TextDecoder() while (content) { const { done, value } = await content.read() if (done) break console.log(done, decode.decode(value)) }
這裏呢我們逐步讀取流,content.read()
是一個非同步操作,從流中讀取下一塊資料。它返回一個包含兩個屬性的物件,done
和value
分別是讀取到的值和是否讀取完畢,當讀取完畢我們直接結束迴圈,但是此刻的value
還是一個Uint8Array
的資料格式,我們需要使用TextDecoder
進行轉換為字串,到這裏就可以拿到數據流推過來的東西了。此刻我們可以再修改一下後臺,修改爲post
請求來實驗一下,並且可以直接返回要推送的值。
app.post('/chat_typing') res.write({data}) const res = await fetch('http://localhost:3000/chat_typing', { method: 'POST', signal: abort.signal, })
這樣我們也就實現了可以使用fetch
並且寫道引數來讀取數據流了,並且使用到了POST請求
結尾
這裏呢主要介紹的就是如何與後臺進行連線實現訊息推送,並且傳遞引數接收數據流,像這樣的訊息推送一般都是用來訊息訂閱,訊息推送,提醒等功能上。