hello~親愛的看官老爺們你們好~過完年第一週已經結束,是時候開始制定新的工做計劃了。主要負責的項目是數據可視化平臺,而使用中若是服務器能有推送能力讓頁端獲得相關通知的話,就能實現不少功能上的優化。鑑於項目中 Node 端已經正式投入使用,前端擁有了本身的服務器,搞事情起來天然方便不少。前端
若干年前,服務器並無主動推送的能力,主要是經過輪詢的方式來達到近似於服務器推送的能力。如今不須要這麼麻煩,輪詢只做爲向下兼容的方案便可,當前主流的服務器推送是使用 SSE 或者 WebSocket 來實現的。二者對好比下:git
是否基於新協議 | 是否雙向通訊 | 是否支持跨域 | 接入成本 | |
---|---|---|---|---|
SSE | 否(Http ) |
否(服務器單向) | 否(Firefox 支持跨域) | 低 |
WebSocket | 是(ws ) |
是 | 是 | 高 |
須要稍微解釋一下的是接入成本。SSE 是相對輕量級的協議,(Node)代碼實現上比較簡單,而 WebSocket 是比較複雜的協議,雖然也有類庫能夠套用,也許頁端方面二者代碼量差很少,但服務器方面實現就複雜很多了。同時,要實現 WebSocket,是須要另起一個服務,而 SSE 並不須要。github
比較以後,對 SSE 與 WebSocket 有了大體的理解。項目對服務器推送的要求是發送通知,而將來可能須要接入實時同步的功能,結合項目實際狀況與接入成本後,選擇了 SSE。跨域
最後看一下瀏覽器支持狀況以做參考:瀏覽器
IE
就 let it go
吧,平常不支持~其餘瀏覽器仍是綠油油的,支持度仍是挺高的。服務器
項目中使用 Egg 做爲框架,底層是 Koa2 的,於是使用 Koa2 做爲示例。Node 端關鍵代碼以下:app
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次,對應規範中的各個字段,理解以下:async
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 上,歡迎查閱。
感謝各位看官大人看到這裏,知易行難,但願本文對你有所幫助~謝謝!