Server-sent?events實(shí)時獲取服務(wù)端數(shù)據(jù)技術(shù)詳解
正文
實(shí)時獲取服務(wù)端的數(shù)據(jù),大家第一時間想到的是輪詢和 WebSocket 兩種方案,其實(shí)還有一種新方案 Server-sent events 下文簡稱(SSE)。SSE 中的數(shù)據(jù)只能由服務(wù)端推向客戶端
SSE 是基于 http 協(xié)議的服務(wù)器推送技術(shù),數(shù)據(jù)只能從服務(wù)端到客戶端。服務(wù)端把序列化后的數(shù)據(jù)發(fā)送給客戶端, 整個過程持續(xù)不斷直至連接關(guān)閉
WebSocket vs 輪詢 vs SSE
下面是 WebSocket、輪詢和 SSE 的功能對比
- SSE 和輪詢使用 HTTP 協(xié)議,現(xiàn)有的服務(wù)器軟件都支持。WebSocket 是一個獨(dú)立協(xié)議
- SSE 屬于輕量級的 WebSocket,使用簡單;WebSocket 使用相對復(fù)雜,輪詢使用簡單
- SSE 默認(rèn)支持?jǐn)嗑€重連,WebSocket 需要自己實(shí)現(xiàn)斷線重連
- SSE 一般只用來傳送文本,二進(jìn)制數(shù)據(jù)需要編碼后傳送,WebSocket 默認(rèn)支持傳送二進(jìn)制數(shù)據(jù)
- SSE 支持自定義發(fā)送的消息類型
- WebSocket 支持雙向推送消息,SSE 是單向的
- 輪詢性能開銷大、輪詢時間久導(dǎo)致客戶端及時更新數(shù)據(jù)
使用場景
基于服務(wù)端單向的向客戶端推送信息的特性,SSE 使用場景主要有
- Sass 平臺的消息通知
- 信息流網(wǎng)站實(shí)時更新數(shù)據(jù)
使用方式
下面講解如何在客戶端使用 SSE
- 創(chuàng)建一個
EventSource
實(shí)例,向服務(wù)器發(fā)起連接
const evtSource = new EventSource();
- 自定義事件
對于自定義事件,服務(wù)端和客戶端一定要保持事件名一致。服務(wù)端通過自定義事件發(fā)送數(shù)據(jù), 就會觸發(fā)自定義事件。SSE 默認(rèn)支持 message
事件,下面以 message
事件為例
evtSource.addEventListener("message", (event) => { let payload; try { payload = JSON.parse(event.data); // <--- event.data 需要反序列化 console.log("receiving data...", payload); } catch (error) { console.error("failed to parse payload from server", error); } });
自定義事件的回調(diào)函數(shù)接收 event
對象,event.data
存著服務(wù)端發(fā)給客戶端的數(shù)據(jù)但是需要反序列化
可以通過 Chrome Devtool 工具查看 eventsource
通信情況,如圖所示
1
- 自定義事件名,服務(wù)端和客戶端需要保持一致2
- EventStream Tab,數(shù)據(jù)都在這里3
- 服務(wù)端推送給客戶端的數(shù)據(jù)
- 錯誤處理
如果連接發(fā)生錯誤,就會觸發(fā) error
事件
evtSource.addEventListener("error", (err) => { console.error("EventSource failed:", err); });
- 關(guān)閉連接
SSE 提供 close
方法,用來關(guān)閉 SSE 連接
evtSource.close();
瀏覽器兼容性
通過 caniuse 查看 SSE 瀏覽器兼容性,如圖所示
除了 IE 瀏覽器不支持,其它現(xiàn)代瀏覽器都支持,所以放心大膽在項(xiàng)目中使用 SSE
簡單封裝
在平常的工作中,每次寫 SSE 的事件監(jiān)聽和錯誤處理會很麻煩。多個業(yè)務(wù)場景需要使用 SSE 時,就需要對 SSE 進(jìn)行封裝。接下來我們嘗試封裝一個簡單的 SSE SDK,方便在項(xiàng)目中使用
當(dāng)我們決定寫 SSE 的 SDK 時,首先想到使用面向?qū)ο螅∣OP)進(jìn)行封裝。根據(jù) SSE 的特性,那么庫需要實(shí)現(xiàn) subscribe
和 unsubscribe
兩個方法。通過確定 SSE
庫使用方式,根據(jù)使用方式確定 SDK 的實(shí)現(xiàn)。我們可以在代碼中這樣使用,如下所示
// SSESdk 實(shí)例化 const SSE = new SSESdk(url, options); // 訂閱來自服務(wù)端的消息 SSE.subscribe("message", (data) => { console.log("receive message from server", data); }); // 取消訂閱 SSE.unsuscribe();
我們要封裝的庫對外僅僅提供 subscribe
和 unsubscribe
兩個 Api,非常方便開發(fā)人員使用。 subscribe
用來訂閱來自服務(wù)端的消息, unsubscribe
用來取消訂閱,關(guān)閉 SSE 連接,通過使用形式可以看出,使用 ES6 中的類語法。接下來我們先確定 SSE SDK 的大體結(jié)構(gòu)
class SSEClient { constructor() {} subscribe(type, handler) {} unsunscribe() {} }
在 SSEClient
類中有三個方法需要實(shí)現(xiàn),通過 constructor 接受可配置的參數(shù),比如 SSE 建立連接失敗后的重試次數(shù)和重試時間。 subscribe
接收一個與后端保持一致的事件名和一個回調(diào)函數(shù)。unsunscribe
不需要傳遞任何參數(shù),調(diào)用 unsunscribe
方法關(guān)閉 SSE 連接
// SSE-client.js class SSEClient { constructor(url) { this.url = url; this.es = null; } subscribe(type, handler) { this.es = new EventSource(url); this.es.addEventListener("open", () => { console.log("server sent event connect created"); }); this.es.addEventListener(type, (event) => { let payload; try { payload = JSON.parse(event.data); console.log("receiving data...", payload); } catch (error) { console.error("failed to parse payload from server", error); } if (typeof handler === "function") { handler(payload); } }); this.es.addEventListener("error", () => { console.error("EventSource connection failed for subscribe.Retry"); }); } unsunscribe() { if (this.es) { this.es.close(); } } }
就這樣實(shí)現(xiàn)了一個簡單的 SSE
SDK。首先根據(jù) url 參數(shù)創(chuàng)建一個 SSEClient
實(shí)例,當(dāng)調(diào)用 subscribe
方法時,才會根據(jù)傳入的 url 建立 SSE
連接,然后監(jiān)聽對應(yīng)的事件,一旦 連接建立成功,后端向客戶端發(fā)送數(shù)據(jù),就可以從 handler
方法中拿到數(shù)據(jù)
這個庫僅僅實(shí)現(xiàn)了非常基本的功能,代碼封裝上存在很多問題。比如 es
的事件全部雜糅在 subscribe
方法中、缺少 SSE
連接建立失敗的重試等等功能。接下來我們對剛剛實(shí)現(xiàn)的 SSEClient
SDK 進(jìn)行優(yōu)化
const defaultOptions = { retry: 5, interval: 3 * 1000, }; class SSEClient { constructor(url, options = defaultOptions) { this.url = url; this.es = null; this.options = options; this.retry = options.retry; this.timer = null; } _onOpen() { console.log("server sent event connect created"); } _onMessage(handler) { return (event) => { this.retry = options.retry; let payload; try { payload = JSON.parse(event.data); console.log("receiving data...", payload); } catch (error) { console.error("failed to parse payload from server", error); } if (typeof handler === "function") { handler(payload); } }; } _onError(type, handler) { return () => { console.error("EventSource connection failed for subscribe.Retry"); if (this.es) { this._removeAllEvent(type, handler); this.unsunscribe(); } if (this.retry > 0) { this.timer = setTimeout(() => { this.subscribe(type, handler); }, this.options.interval); } else { this.retry--; } }; } _removeAllEvent(type, handler) { this.es.removeEventListener("open", this._onOpen); this.es.removeEventListener(type, this._onMessage(handler)); this.es.removeEventListener("error", this._onError(type, handler)); } subscribe(type, handler) { this.es = new EventSource(url); this.es.addEventListener("open", this._onOpen); this.es.addEventListener(type, this._onMessage(handler)); this.es.addEventListener("error", this._onError(type, handler)); } unsunscribe() { if (this.es) { this.es.close(); this.es = null; } if (this.timer) { clearTimeout(this.timer); } } }
我們將 SSEClient
中的三個事件方法分別提取為三個私有方法,_onOpen
方法在 event 觸發(fā) open 時調(diào)用,向控制臺輸出鏈接已經(jīng)創(chuàng)建。 _onMessage
方法在后端向前端發(fā)送數(shù)據(jù)時觸發(fā),負(fù)責(zé)解析數(shù)據(jù),并調(diào)用 handler
方法。_onError
方法在 SSE 發(fā)生錯誤時觸發(fā), 會在控制臺輸出錯誤的提示,根據(jù)開發(fā)者傳入的重試次數(shù),先關(guān)閉上一次的 SSE 鏈接,取消所有的事件監(jiān)聽,關(guān)閉定時器, 再開啟遞歸調(diào)用 subscribe
方法進(jìn)行重連, 一旦重連成功,重試次數(shù)恢復(fù)為設(shè)定的重試次數(shù),如果超過重試次數(shù)依舊沒有連接成功,那么 SSE
會徹底終止。需要開發(fā)人員排查具體原因
一個可以用在項(xiàng)目上的簡單 SSE
SDK 封裝完
第三方庫
SSE
雖然很好,但是也有它先天不足,主要問題是不能通過 headers
傳遞 Authorization token
。雖然可以把 token 放在 url 上 解決不能傳 token
的問題,但是又會引發(fā) token
安全隱患。所以社區(qū)里有使用 xhr
和 fetch
模擬原生 Server-sent events
的功能,解決不能 通過 headers
傳遞 Authorization token
的問題。主要有兩個第三方庫,分別是 eventsource
和 event-source-polyfill
, 下面筆者詳細(xì)講述這兩個庫的使用
eventsource
此庫是 EventSource 客戶端的純 JavaScript 實(shí)現(xiàn)。使用方式很簡單。在項(xiàng)目中安裝依賴
yarn add eventsource # Or npm install eventsource
然后從 eventsource
中導(dǎo)出 EventSource
類,然后實(shí)例化得到 es
實(shí)例
import EventSource from "eventsource"; const eventSourceInitDict = { headers: { authorization: "Bearer token" } }; const es = new EventSource(url, eventSourceInitDict); es.addEventListener("message", (event) => { console.log("receiving data from server:", JSON.parse(event.data)); });
eventsource
的實(shí)現(xiàn)用到了一些 node
標(biāo)準(zhǔn)庫。分別是 https
和 http
。 筆者將 eventsource
的部分源碼列在下面。
// eventsource.js 源碼如下 const https = require("https"); const http = require("http");
然而,瀏覽器環(huán)境并不支持 https
和 http
標(biāo)準(zhǔn)庫。所以當(dāng)我們在瀏覽器環(huán)境中使用 eventsource
時,需要做一些額外的工作。下面以 webpack5 為例子講解解決辦法
- 需要在
webpack
配置文件中添加node-polyfill-webpack-plugin
插件
yarn add node-polyfill-webpack-plugin -D
然后在 webpack
配置文件使用該插件
// 項(xiàng)目中的 webpack 配置文件,比如 webpack.config.js const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); module.exports = { // Other rules... plugins: [new NodePolyfillPlugin()], };
- 或者在
webpack
的callback
中對使用的庫進(jìn)行單獨(dú)的配置
module.exports = { // other configuration ... resolve: { fallback: { https: false, http: false, }, }, };
做完上面的步驟后,eventsource
可以在瀏覽器中正常運(yùn)行
如果不想改動 webpack
的配置,那么可以試試 event-source-polyfill
這個庫
event-source-polyfill
event-source-polyfill 的使用非常簡單,使用 EventSourcePolyfill
替換原生的 EventSource
import { EventSourcePolyfill } from "event-source-polyfill"; var es = new EventSourcePolyfill(url, { headers: { authorization: "Bearer token", }, }); es.addEventListener("message", (event) => { console.log("receiving data from server:", JSON.parse(event.data)); });
不足之處
eventsource
和 event-source-polyfill
只是在一定的程度上解決了 Authorization token
的問題,但它們也存在問題。 這兩個庫提供的 close
方法只能關(guān)閉處于 pending
狀態(tài)的 SSE 連接,因?yàn)?fetch 一旦從 pending
變?yōu)?resolved
或 reject
, 其結(jié)果無法改變。當(dāng)頻繁的斷開 SSE 連接和建立新 SSE 連接時,舊的 SSE 連接實(shí)際上并沒有關(guān)閉,系統(tǒng)里會存在多個 SSE 連接,這樣會帶來很大的性能開銷
FAQ
- SSE 不能向服務(wù)端發(fā)送數(shù)據(jù)?
可以將數(shù)據(jù)放入 url
中,斷開當(dāng)前的 SSE 連接,根據(jù)新 url 重新建立 SSE 連接
總結(jié)
本篇文章講述一種服務(wù)端向客戶端推送信息的技術(shù)、它比 WebSocket
更簡單更輕量化,比輪詢性能好。 簡單介紹 Server-sent events
的技術(shù)原理和使用場景,并進(jìn)行簡單的封裝,方便日常在項(xiàng)目中使用。推薦使用 eventsource
和 event-source-polyfill
第三方庫解決不能通過 headers
傳遞 Authorization token
的問題。
參考鏈接 Server-sent events
以上就是Server-sent events實(shí)時獲取服務(wù)端數(shù)據(jù)技術(shù)詳解的詳細(xì)內(nèi)容,更多關(guān)于Server-sent events的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript?ES6語法中l(wèi)et,const?,var?的區(qū)別
這篇文章主要為大家介紹了JavaScript中l(wèi)et,const?,var?的區(qū)別,具有一定的參考價值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來幫助2022-01-01rollup打包引發(fā)對JS模塊循環(huán)引用思考
這篇文章主要為大家介紹了rollup打包引發(fā)的對JS模塊循環(huán)引用的思考,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08JavaScript loader原理簡單總結(jié)示例解析
這篇文章主要為大家介紹了JavaScript loader原理簡單總結(jié)示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08ECMAScript 6數(shù)值擴(kuò)展實(shí)例詳解
這篇文章主要為大家介紹了ECMAScript6數(shù)值擴(kuò)展實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08