使用 server-sent 事件
=================

開發一個使用 server-sent 事件的網頁應用程式很簡單。在伺服器端只需要一些的程式碼與網頁串流事件，而客戶端這邊的處理進入事件的部分幾乎跟 websockets 一樣。這是一種單向的連線，所以你無法從客戶端向伺服器傳送事件。

從伺服器端接收事件
---------

server-sent event API 包含在 `EventSource` (en-US) 介面；為了與伺服器端開啟連線並接收事件，需要建立帶有產生事件 script URL 的 `EventSource` (en-US) 物件。例如：


```js
const evtSource = new EventSource("ssedemo.php");
```


如果事件產生的 script 在不同源的伺服器上，在建立 `EventSource` (en-US) 物件時需要同時提供 URL 和第二個參數作為選項設定。假設客戶端的 script 伺服於 `example.com` ：


```js
const evtSource = new EventSource("//api.example.com/ssedemo.php", {
  withCredentials: true,
});
```


當你完成初始化事件來源後，你就可以透過新增 `message` (en-US) 事件的處理器來開始監聽來自伺服器的訊息：


```js
evtSource.onmessage = function (event) {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");

  newElement.innerHTML = "message: " + event.data;
  eventList.appendChild(newElement);
};
```


上述的程式碼會監聽進入的訊息（這裡來自伺服器的通知沒有指明 `event` 欄位，所以統一用 `onmessage` 處理即可）並且把訊息的文字附加到 document 的清單。

你也可以利用 `addEventListener()` 監聽事件：


```js
evtSource.addEventListener("ping", function (event) {
  const newElement = document.createElement("li");
  const time = JSON.parse(event.data).time;
  newElement.innerHTML = "ping at " + time;
  eventList.appendChild(newElement);
});
```


上述的程式碼大同小異，不同之處在於若伺服器傳送了 `event` 欄位值為「ping」的訊息時它就會把 `data` 欄位的值解析為 JSON 並輸出到畫面上。

**警告：** 當連線不是透過 **HTTP/2** 時，SSE 會受到最大連線數限制所苦，尤其當開啟多個分頁。每個瀏覽器有自己的限制數而且被限制在很低的數量（6）。這個問題已經被 Chrome 和 Firefox 標註為「Won't fix」（不修復）。限制是基於每個瀏覽器 + 網域，也就是說你可以針對 www.example1.com 網域在所有的分頁中開啟六個 SSE 連線，另一個網域 www.example2.com 也可以開啟六個（根據 Stackoverflow）。當使用 HTTP/2 時最大同時 *HTTP streams* 連線數是由伺服器和客戶端之間協調（預設 100）。

從伺服器發送事件
--------

由伺服器端所發送的事件需要使用 `text/event-stream` 的 MIME 類型回應。每一個通知皆由一組文字組成並由一對換行結尾。如何處理事件串流的格式，請參考 Event stream format 。

下面是一個 PHP 範例：


```php
date\_default\_timezone\_set("America/New\_York");
header("Cache-Control: no-cache");
header("Content-Type: text/event-stream");

$counter = rand(1, 10);
while (true) {
  // Every second, send a "ping" event.

  echo "event: ping\n";
  $curDate = date(DATE\_ISO8601);
  echo 'data: {"time": "' . $curDate . '"}';
  echo "\n\n";

  // Send a simple message at random intervals.

  $counter--;

  if (!$counter) {
    echo 'data: This is a message at time ' . $curDate . "\n\n";
    $counter = rand(1, 10);
  }

  ob\_end\_flush();
  flush();

  // Break the loop if the client aborted the connection (closed the page)

  if ( connection\_aborted() ) break;

  sleep(1);
}
```


 上述的程式碼會在每秒產生一個類型為「ping」的事件。每一個事件的資料是一個 JSON 物件，內容為事件產生時的 ISO 8601 時間戳。同時會隨機發送一個簡易訊息（沒有事件類型）。
 迴圈的執行會獨立於連線的狀態，，所以在迴圈裡必須檢查連線的狀態，若斷線了要關閉連線（譬如，客戶端關閉了網頁）。

**備註：** 你可以從下列的 Github 文章中找到包含本文所使用程式碼的完整範例 —— 參考 Simple SSE demo using PHP.

錯誤處理
----

當錯誤發生時（譬如網路逾時或有關存取控制的問題）會產生錯誤事件。你可以透過對 `EventSource` 物件實作 `onerror` 回呼的方式採取程式化的處理：


```js
evtSource.onerror = function (err) {
  console.error("EventSource failed:", err);
};
```

關閉事件串流
------

預設的情況下，如果客戶端和伺服器的連線關閉則連線會被重啟。連線的關閉會伴隨著 `.close()` 方法的執行。
```

evtSource.close();
```

事件串流（Event Stream）格式
--------------------

事件串流是個簡易的文字資料串流，內容必須以 UTF-8 格式編碼。在事件串流中，不同的訊息以一對換行符號做區隔。若要撰寫註解，則要在該行的開頭加上冒號（:）。

**備註：** 註解將有助於防止連線逾時；伺服器端可以定時發送註解以維持連線活著。

每一個訊息是由一到多列的欄位所組成的文字。每個欄位依序由欄位的名稱、冒號、該欄位的文字內容所組合而成。

### 欄位

每隔訊息皆可以由下列的欄位組合而成，每個欄位以換行做為區隔：

`event`

事件的類型。如果有指定則在瀏覽器端會對該事件名稱的監聽器發布事件；網頁的原始碼必須使用 `addEventListener()` 來監聽已命名的事件。 `onmessage` 處理器只有在訊息沒有指定事件名稱時才會被呼叫。

`data`

訊息的資料欄位。當 EventSource 連續接收到多列以 `data:` 開頭的內容；它會串接這些內容並為每一列插入一個換行字元。最後的換行會被移除。

`id`

`EventSource` (en-US) 物件的最新一個事件 ID 。

`retry`

當嘗試傳送事件時重新連線的時間。這個值必須是整數，單位是毫秒，作為重新連線的時間。若指定是非整數則這個欄位會被忽略。

除上述的幾個欄位，其他欄位均會被忽略。

**備註：** 如果某列的內容沒有包含冒號，則該列的內容都會被視為欄位名稱及空字串的欄位值。

### 範例

#### Data-only 訊息

在下列的範例中，共發送了三個訊息。第一個是註解，因其以冒號開頭。如之前提到的，對不會持續發送訊息的情境下，這將有助於維持連線的存續。

第二則訊息包含了 data 欄位及「some text」的值。第三則訊息包含了 data 欄位及「another message\nwith two lines」。注意在內容所出現的換行符號。
```

: this is a test stream

data: some text

data: another message
data: with two lines
```


#### 命名事件

這個範例傳送了一些命名的事件。每一個事件都被指定了 `event` 欄位並且 `data` 欄位也有相應的 JSON 字串作為客戶端回應事件所需的資料。 `data` 欄位的內容可以是任何的字串；它並沒有強制必須以 JSON 的格式撰寫。
```

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}

event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}

event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}
```


#### 混合及配對

你並非一定只能用未命名訊息或已分類的事件；實際上你可以在單一的事件中把它們混合在一起。
```

event: userconnect
data: {"username": "bobby", "time": "02:33:48"}

data: Here's a system message of some kind that will get used
data: to accomplish some task.

event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
```

### EventSource

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.