server-side-events(SSE)開發指南(Node)
SSE是介於websocket、長短輪訓之外的一種服務端推送的方式。他的好處有
- 使用簡單,無需藉助第三方庫(如socket.io)
- 基於HTTP協議(Socket/">WebSocket 是一個獨立協議),無需對其做額外處理。還能享受HTTP2帶來的優勢
- 預設支援斷線重連
- 支援自定義傳送的訊息型別
詳細對比,這裡我選擇嘗試將一個原本基於輪詢的web app轉到sse上來。雖然這套技術看上去使用很簡單,但可能由於普及程度不高和資料較少的原因,在開發過程中會遇到很多的坑和要面臨的新東西。這裡幫大家總結一下,後端使用了koa.js(express應該會更簡單)。
後端
對於一個SSE相應我們需要返回如下一些HTTP頭
Content-Type: text/event-stream Cache-Control: no-cache, no-transform Connection: keep-alive X-Accel-Buffering: no 複製程式碼
在其他的教程中提供的http頭可能沒有這裡的全,區別主要在於:
- Cache-Control中需要包含no-transform,沒有這個的話在開發中,如果你用了create-react-app等工具來轉發你的請求,那麼你的資料流很可能被壓縮,造成你怎麼也收不到響應。這裡當時排查了蠻久的( ofollow,noindex">issue )
- no-transform是開發環境中的遇到的問題,但是在生產環境仍然還存在問題,比如我的網站使用nginx做反向代理的,預設會對應用的響應做緩衝(buffering),以至於我應用返回的訊息沒有立馬發出去。所以我們需要給http頭加上一條X-Accel-Buffering: no(issue)
當設定好header後,我們就可以寫入資料了。一般來說我們只需要監聽資料的更新然後使用 res.write
即可寫入資料:
const onEvent = function(data) { res.write(`event: message\n`); res.write(`data: ${JSON.stringify(data)}\n\n`); }; emitter.on('message', onEvent); 複製程式碼
我們用 \n
來分隔每一行資料,用 \n\n
來分隔每一個事件。每一個事件中包含事件的type和事件的data,分別用兩行來描述。比如上面是返回來一個message事件(若不指定事件型別,則預設message)。下圖中我們還返回來一個withdraw事件,對應的資料行應該是 event: withdraw
。

koa.js返回
對於koa情況比較複雜,官方不推薦我們直接操作res物件,而是給context(ctx)物件的body賦值。 官方例子
其實我們只需要給ctx.body賦一個可寫流,關於node流的概念可以看taobao的這篇文章。如官方示例的:
/** * Create a transform stream that converts a stream * to valid `data: <value>\n\n' events for SSE. */ var Transform = require('stream').Transform; var inherits = require('util').inherits; module.exports = SSE; inherits(SSE, Transform); function SSE(options) { if (!(this instanceof SSE)) return new SSE(options); options = options || {}; Transform.call(this, options); } SSE.prototype._transform = function(data, enc, cb) { this.push(data.toString('utf8')); cb(); }; 複製程式碼
注意官方例項中有個坑就是預設給每行資料前面加上了 data:
字首,這裡刪除了。在使用 const body = ctx.body = SSE()
後就可以對body物件使用 body.write
了。詳見官方例項,例項db.jsw檔案中的可讀流是可選項。
客戶端
客戶端(瀏覽器)的使用就非常簡單了。大部分的瀏覽器支援SSE,而且我們有針對老瀏覽器的相容方案,如 Yaffle 。

使用上真的是特別的簡單,而且幾乎沒有什麼坑
const evtSource = new EventSource('/events'); evtSource.addEventListener('event', function(evt) { const data = JSON.parse(evt.data); // Use data here }, false); 複製程式碼
上面的 event
可以替換為你的其他自定義事件。注意這裡的連線中斷後會自動重連,也許你需要監聽onerror事件來做一些額外的處理(API)。導致中斷的原因可能有時間間隔到期、網路錯誤等。你可以通過定時向客戶端返回內容來避免間隔到期:
// Heartbeat const nln = function() { res.write('\n'); }; const hbt = setInterval(nln, 15000); // Clear heartbeat and listener req.on('close', function() { clearInterval(hbt); emitter.removeListener('event', onEvent); }); 複製程式碼
將輪詢替換為sse後還是很清爽的。注意和websocket不同sse是單向資料流,我們在傳送訊息的時候需要使用其它的介面,可以通過node的events來監聽觸發推送。