server-side-events(SSE)開發指南(Node)

Skandar-Ln發表於2019-03-01

server-side-events(SSE)開發指南(Node)

SSE是介於websocket、長短輪訓之外的一種服務端推送的方式,用資料流的形式傳送文字資料,可想象成網路視訊的文字版。他的好處有

  • 使用簡單,無需藉助第三方庫(如 socket.io
  • 基於HTTP協議(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等工具來轉發你的請求,那麼你的資料流很可能被壓縮,造成你怎麼也收不到響應。這裡當時排查了蠻久的(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

events

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.js檔案中的可讀流是可選項。

客戶端

客戶端(瀏覽器)的使用就非常簡單了。大部分的瀏覽器支援SSE,而且我們有針對老瀏覽器的相容方案,如Yaffle

server-side-events(SSE)開發指南(Node)

使用上真的是特別的簡單,而且幾乎沒有什麼坑

 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來監聽觸發推送。

參考資料

相關文章