hello~親愛的看官老爺們大家好~過完年第一週已經結束,是時候開始制定新的工作計劃了。主要負責的專案是資料視覺化平臺,而使用中如果伺服器能有推送能力讓頁端得到相關通知的話,就能實現很多功能上的優化。鑑於專案中 Node 端已經正式投入使用,前端擁有了自己的伺服器,搞事情起來自然方便很多。
技術選型:SSE(Server-sent Events) or WebSocket
若干年前,伺服器並沒有主動推送的能力,主要是通過輪詢的方式來達到近似於伺服器推送的能力。現在不需要這麼麻煩,輪詢只作為向下相容的方案即可,當前主流的伺服器推送是使用 SSE 或者 WebSocket 來實現的。兩者對比如下:
是否基於新協議 | 是否雙向通訊 | 是否支援跨域 | 接入成本 | |
---|---|---|---|---|
SSE | 否(Http ) |
否(伺服器單向) | 否(Firefox 支援跨域) | 低 |
WebSocket | 是(ws ) |
是 | 是 | 高 |
需要稍微解釋一下的是接入成本。SSE 是相對輕量級的協議,(Node)程式碼實現上比較簡單,而 WebSocket 是比較複雜的協議,雖然也有類庫可以套用,也許頁端方面兩者程式碼量差不多,但伺服器方面實現就複雜不少了。同時,要實現 WebSocket,是需要另起一個服務,而 SSE 並不需要。
比較之後,對 SSE 與 WebSocket 有了大致的理解。專案對伺服器推送的要求是傳送通知,而未來可能需要接入實時同步的功能,結合專案實際情況與接入成本後,選擇了 SSE。
最後看一下瀏覽器支援情況以作參考:
IE
就 let it go
吧,日常不支援~其他瀏覽器還是綠油油的,支援度還是挺高的。
示例
Node端
專案中使用 Egg 作為框架,底層是 Koa2 的,因而使用 Koa2 作為示例。Node 端關鍵程式碼如下:
app.use(async (ctx) => {
const { res, request: { url } } = ctx;
res.writeHead(200, {
'Content-Type': 'text/event-stream', // 伺服器宣告接下來傳送的是事件流
});
let stream = new PassThrough();
let i = 0;
let timer = setInterval(() => {
if (i === 5) {
stream.write('event: pause\n'); // 事件型別
} else {
stream.write('event: test\n'); // 事件型別
}
stream.write(`id: ${+new Date()}\n`); // 訊息ID
stream.write(`data: ${i}\n`); // 訊息資料
stream.write('retry: 10000\n'); // 重連時間
stream.write('\n\n'); // 訊息結束
i++;
}, 1000);
stream.on('close', function() {
console.log('closed.')
clearInterval(timer);
})
ctx.body = stream;
});
複製程式碼
伺服器告訴客戶端,返回的型別是事件流(text/event-stream),查閱 MDN 文件可知:事件流僅僅是一個簡單的文字資料流,文字應該使用 UTF- 8 格式的編碼。每條訊息後面都由一個空行作為分隔符。以冒號開頭的行為註釋行,會被忽略。
之後就是訊息主體了,儘管例子使用 setInterval
模擬不斷髮送推送,但換成任意條件觸發推送也是可以的。stream.write
呼叫了5次,對應規範中的各個欄位,理解如下:
event
為訊息的事件型別。客戶端在EventSource
中可以通過addEventListener
收聽相關的訊息。該欄位可省略,省略後客戶端觸發message
事件。id
為事件 ID。作為客戶端內部的“最後一個事件 ID”的屬性值,用於重連,不可省略。data
為訊息的資料欄位,簡單說就是客戶端監聽時間後,通過e.data
拿到的資料。retry
為重連時間,可省略該引數。- 最後是結束該次通知的
\n\n
,不可省略。除了上面規定的欄位名,其他所有的欄位名都會被忽略。
更詳細的解釋可以查閱 MDN 文件。有一個小細節需要注意,在 SSE 的草案中提到,"text/event-stream" 的 MIME 型別傳輸應當在靜置 15 秒後自動斷開。但實測(僅用了 Chrome)後發現,即使靜置時間超過 15 秒,瀏覽器與客戶端均不會斷開連線。查閱了不少文章,均建議維護一套傳送 \n\n
的心跳機制。個人認為此舉有助於提高客戶端程式的健壯性,但不是必須的。
最後是監聽事件流的 close
事件,用於結束此次的連結。測試後發現,無論是讓客戶端呼叫 close
方法(下文有例子~),還是異常結束,包括關閉視窗、關閉程式等,都能觸發伺服器的 close
事件。
客戶端
客戶端程式碼更簡單,示例如下:
const source = new EventSource('http://localhost:3000/test');
source.addEventListener('open', () => {
console.log('Connected');
}, false);
source.addEventListener('message', e => {
console.log(e.data);
}, false);
source.addEventListener('pause', e => {
source.close();
}, false);
複製程式碼
前端童鞋對於這樣的程式碼應該挺熟悉的,一切都是事件觸發,根據不同的事件執行對應的程式碼。稍微說明一下 EventSource
擁有的屬性和方法,相信大家就可以愉快地使用了。
EventSource
有三個預設的事件,分別是:
open
:在連線開啟時被呼叫。message
:收到一個沒有 event 屬性的訊息時被呼叫。error
:當發生錯誤時被呼叫。
兩個只讀屬性:
readyState
:代表連線狀態。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。url
:代表連線的 URL。
一個方法:
close
:呼叫後關閉連線(也就是上文所提及的)。
更詳細的解釋可以查閱 MDN 文件
小結
關於伺服器 SSE 的簡單介紹就到此為止了,可以看到,SSE 開發起來還是比較簡單的,接入成本非常低。但並不是說 WebSocket 就是不好的,拋開實際場景談業務就是耍流氓。此外上述程式碼只是演示,還能進一步進行優化的。如為了減輕伺服器開銷,可以建立一套機制有目的地斷開與重連等,大家可以自行實現。
相關的程式碼已經丟到 Github 上,歡迎查閱。
感謝各位看官大人看到這裡,知易行難,希望本文對你有所幫助~謝謝!