2020 年 11 月 30 日

伺服器傳送事件

伺服器傳送事件 規範說明了一個內建類別 EventSource,它會與伺服器保持連線,並允許從伺服器接收事件。

WebSocket 類似,連線是持續的。

但有幾個重要的差異

WebSocket EventSource
雙向:客戶端和伺服器都可以交換訊息 單向:只有伺服器會傳送資料
二進制和文字資料 只有文字
WebSocket 協定 一般 HTTP

EventSource 是一種與伺服器溝通的方式,功能比 WebSocket 弱。

為什麼有人會使用它?

主要原因:它比較簡單。在許多應用程式中,WebSocket 的功能有點太強大了。

我們需要從伺服器接收資料串流:可能是聊天訊息、市場價格,或其他任何東西。這就是 EventSource 擅長的地方。它還支援自動重新連線,這是我們需要使用 WebSocket 手動實作的功能。此外,它是一種傳統的 HTTP,而不是一種新協定。

取得訊息

要開始接收訊息,我們只需要建立 new EventSource(url)

瀏覽器會連線到 url 並保持連線開啟,等待事件。

伺服器應以狀態碼 200 和標頭 Content-Type: text/event-stream 回應,然後保持連線並以特殊格式將訊息寫入其中,如下所示

data: Message 1

data: Message 2

data: Message 3
data: of two lines
  • 訊息文字出現在 data: 之後,冒號後的空格是可選的。
  • 訊息以雙換行符號 \n\n 分隔。
  • 要傳送換行符號 \n,我們可以立即再傳送一個 data:(上方的第 3 個訊息)。

在實際應用中,複雜的訊息通常會以 JSON 編碼傳送。換行符號會在其中編碼為 \n,因此不需要多行的 data: 訊息。

例如

data: {"user":"John","message":"First line\n Second line"}

…因此,我們可以假設一個 data: 僅包含一個訊息。

對於每個這樣的訊息,都會產生 message 事件

let eventSource = new EventSource("/events/subscribe");

eventSource.onmessage = function(event) {
  console.log("New message", event.data);
  // will log 3 times for the data stream above
};

// or eventSource.addEventListener('message', ...)

跨來源請求

EventSource 支援跨來源請求,例如 fetch 和任何其他網路方法。我們可以使用任何 URL

let source = new EventSource("https://another-site.com/events");

遠端伺服器會取得 Origin 標頭,並且必須以 Access-Control-Allow-Origin 回應才能繼續進行。

要傳遞憑證,我們應設定額外的選項 withCredentials,如下所示

let source = new EventSource("https://another-site.com/events", {
  withCredentials: true
});

請參閱章節 Fetch:跨來源請求 以取得有關跨來源標頭的更多詳細資訊。

重新連線

建立後,new EventSource 會連線到伺服器,如果連線中斷,則會重新連線。

這非常方便,因為我們不必擔心它。

重新連線之間會有一小段延遲,預設為幾秒鐘。

伺服器可以在回應中使用 retry: 設定建議延遲(以毫秒為單位)

retry: 15000
data: Hello, I set the reconnection delay to 15 seconds

retry: 可能與一些資料一起出現,或作為獨立訊息出現。

瀏覽器應該在重新連線之前等待這麼多毫秒。或者更長,例如,如果瀏覽器知道(來自作業系統)目前沒有網路連線,它可能會等到連線出現後再重試。

  • 如果伺服器希望瀏覽器停止重新連線,它應該回應 HTTP 狀態 204。
  • 如果瀏覽器想要關閉連線,它應該呼叫 eventSource.close()
let eventSource = new EventSource(...);

eventSource.close();

此外,如果回應具有不正確的 Content-Type 或其 HTTP 狀態與 301、307、200 和 204 不同,則不會重新連線。在這種情況下,將發出 "error" 事件,瀏覽器不會重新連線。

請注意

當連線最終關閉時,沒有辦法「重新開啟」它。如果我們想要重新連線,只需建立一個新的 EventSource

訊息 ID

當連線因網路問題而中斷時,任何一方都無法確定哪些訊息已收到,哪些訊息未收到。

為了正確恢復連線,每個訊息都應該有一個 id 欄位,如下所示

data: Message 1
id: 1

data: Message 2
id: 2

data: Message 3
data: of two lines
id: 3

當收到具有 id: 的訊息時,瀏覽器

  • 將屬性 eventSource.lastEventId 設定為其值。
  • 在重新連線時,使用該 id 傳送標頭 Last-Event-ID,以便伺服器可以重新傳送後續訊息。
id: 放在 data: 之後

請注意:id 由伺服器附加在訊息 data 的下方,以確保在收到訊息後更新 lastEventId

連線狀態:readyState

EventSource 物件具有 readyState 屬性,它有三個值之一

EventSource.CONNECTING = 0; // connecting or reconnecting
EventSource.OPEN = 1;       // connected
EventSource.CLOSED = 2;     // connection closed

當建立物件或連線中斷時,它總是 EventSource.CONNECTING(等於 0)。

我們可以查詢此屬性以了解 EventSource 的狀態。

事件類型

預設情況下,EventSource 物件會產生三個事件

  • message – 收到的訊息,可用作 event.data
  • open – 連線已開啟。
  • error – 無法建立連線,例如伺服器傳回 HTTP 500 狀態。

伺服器可以在事件開始時使用 event: ... 指定另一種類型的事件。

例如

event: join
data: Bob

data: Hello

event: leave
data: Bob

要處理自訂事件,我們必須使用 addEventListener,而不是 onmessage

eventSource.addEventListener('join', event => {
  alert(`Joined ${event.data}`);
});

eventSource.addEventListener('message', event => {
  alert(`Said: ${event.data}`);
});

eventSource.addEventListener('leave', event => {
  alert(`Left ${event.data}`);
});

完整範例

以下是一個伺服器,它會傳送訊息 123,然後是 bye,並中斷連線。

然後瀏覽器會自動重新連線。

結果
server.js
index.html
let http = require('http');
let url = require('url');
let querystring = require('querystring');
let static = require('node-static');
let fileServer = new static.Server('.');

function onDigits(req, res) {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream; charset=utf-8',
    'Cache-Control': 'no-cache'
  });

  let i = 0;

  let timer = setInterval(write, 1000);
  write();

  function write() {
    i++;

    if (i == 4) {
      res.write('event: bye\ndata: bye-bye\n\n');
      clearInterval(timer);
      res.end();
      return;
    }

    res.write('data: ' + i + '\n\n');

  }
}

function accept(req, res) {

  if (req.url == '/digits') {
    onDigits(req, res);
    return;
  }

  fileServer.serve(req, res);
}


if (!module.parent) {
  http.createServer(accept).listen(8080);
} else {
  exports.accept = accept;
}
<!DOCTYPE html>
<script>
let eventSource;

function start() { // when "Start" button pressed
  if (!window.EventSource) {
    // IE or an old browser
    alert("The browser doesn't support EventSource.");
    return;
  }

  eventSource = new EventSource('digits');

  eventSource.onopen = function(e) {
    log("Event: open");
  };

  eventSource.onerror = function(e) {
    log("Event: error");
    if (this.readyState == EventSource.CONNECTING) {
      log(`Reconnecting (readyState=${this.readyState})...`);
    } else {
      log("Error has occured.");
    }
  };

  eventSource.addEventListener('bye', function(e) {
    log("Event: bye, data: " + e.data);
  });

  eventSource.onmessage = function(e) {
    log("Event: message, data: " + e.data);
  };
}

function stop() { // when "Stop" button pressed
  eventSource.close();
  log("eventSource.close()");
}

function log(msg) {
  logElem.innerHTML += msg + "<br>";
  document.documentElement.scrollTop = 99999999;
}
</script>

<button onclick="start()">Start</button> Press the "Start" to begin.
<div id="logElem" style="margin: 6px 0"></div>

<button onclick="stop()">Stop</button> "Stop" to finish.

摘要

EventSource 物件會自動建立一個持續連線,並允許伺服器透過它傳送訊息。

它提供

  • 自動重新連線,並可調整 retry 超時。
  • 訊息識別碼,用於繼續事件,重新連線時,會在 Last-Event-ID 標頭中傳送最後接收到的識別碼。
  • 目前的狀態在 readyState 屬性中。

這使得 EventSource 變成 WebSocket 的可行替代方案,因為後者是較低層級的,而且缺乏此類內建功能(儘管它們可以被實作)。

在許多實際應用中,EventSource 的功能就已經足夠了。

支援所有現代瀏覽器(不包括 IE)。

語法是

let source = new EventSource(url, [credentials]);

第二個引數只有一個可能的選項:{ withCredentials: true },它允許傳送跨來源憑證。

整體的跨來源安全性與 fetch 和其他網路方法相同。

EventSource 物件的屬性

readyState
目前的連線狀態:EventSource.CONNECTING (=0)EventSource.OPEN (=1)EventSource.CLOSED (=2)
lastEventId
最後接收到的 id。重新連線時,瀏覽器會在標頭 Last-Event-ID 中傳送它。

方法

close()
關閉連線。

事件

message
訊息已接收,資料在 event.data 中。
open
連線已建立。
error
發生錯誤時,包括連線中斷(會自動重新連線)和致命錯誤。我們可以檢查 readyState 來查看是否正在嘗試重新連線。

伺服器可以在 event: 中設定自訂事件名稱。此類事件應使用 addEventListener 處理,而不是 on<event>

伺服器回應格式

伺服器傳送訊息,以 \n\n 分隔。

訊息可能包含下列欄位

  • data: – 訊息主體,多個 data 的序列會被解釋為單一訊息,各部分之間以 \n 分隔。
  • id: – 更新 lastEventId,重新連線時會在 Last-Event-ID 中傳送。
  • retry: – 建議重新連線的重試延遲時間(毫秒)。沒有辦法從 JavaScript 設定它。
  • event: – 事件名稱,必須在 data: 之前。

訊息可以包含一個或多個欄位,順序不拘,但通常 id: 會放在最後。

教學課程地圖

留言

留言前請先閱讀…
  • 如果你有建議要如何改進 - 請 提交 GitHub 議題 或發起 pull request,不要留言。
  • 如果你看不懂文章中的某個部分 - 請說明。
  • 要插入少數幾個字的程式碼,請使用 <code> 標籤,要插入多行程式碼,請用 <pre> 標籤將它們包起來,要插入超過 10 行的程式碼 - 請使用沙盒 (plnkrjsbincodepen…)