aboutsummaryrefslogtreecommitdiff
path: root/files/zh-tw/web/api/server-sent_events/using_server-sent_events/index.html
blob: e9d5992197b2e030b9cb6a290d99be47d7fb4a4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
---
title: 使用 server-sent 事件
slug: Web/API/Server-sent_events/Using_server-sent_events
tags:
  - Advanced
  - Communication
  - DOM
  - EventSource
  - Guide
  - SSE
  - Server Sent Events
  - Server-Sent-Event
translation_of: Web/API/Server-sent_events/Using_server-sent_events
---
<p>{{DefaultAPISidebar("Server Sent Events")}}</p>

<div class="summary">
<p>開發一個使用 server-sent 事件的網頁應用程式很簡單。在伺服器端只需要一些的程式碼與網頁串流事件,而客戶端這邊的處理進入事件的部分幾乎跟 <a href="/zh-TW/docs/Web/API/WebSockets_API">websockets</a> 一樣。這是一種單向的連線,所以你無法從客戶端向伺服器傳送事件。</p>
</div>

<h2 id="從伺服器端接收事件">從伺服器端接收事件</h2>

<p>server-sent event API 包含在  {{domxref("EventSource")}} 介面;為了與伺服器端開啟連線並接收事件,需要建立帶有產生事件 script URL 的 {{domxref("EventSource")}} 物件。例如:</p>

<pre class="brush: js notranslate">const evtSource = new EventSource("ssedemo.php");</pre>

<p>如果事件產生的 script 在不同源的伺服器上,在建立 {{domxref("EventSource")}} 物件時需要同時提供 URL 和第二個參數作為選項設定。假設客戶端的 script 伺服於 <code>example.com</code></p>

<pre class="brush: js notranslate">const evtSource = new EventSource("//api.example.com/ssedemo.php", { withCredentials: true } );</pre>

<p>當你完成初始化事件來源後,你就可以透過新增 {{event("message")}} 事件的處理器來開始監聽來自伺服器的訊息:</p>

<pre class="brush: js notranslate">evtSource.onmessage = function(event) {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");

  newElement.innerHTML = "message: " + event.data;
  eventList.appendChild(newElement);
}
</pre>

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

<p>你也可以利用 <code>addEventListener()</code> 監聽事件:</p>

<pre class="brush: js notranslate">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);
});
</pre>

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

<div class="blockIndicator warning">
<p>當連線不是透過<strong> HTTP/2</strong> 時,SSE 會受到最大連線數限制所苦,尤其當開啟多個分頁。每個瀏覽器有自己的限制數而且被限制在很低的數量(6)。這個問題已經被 <a href="https://bugs.chromium.org/p/chromium/issues/detail?id=275955" rel="noreferrer">Chrome</a><a href="https://bugzilla.mozilla.org/show_bug.cgi?id=906896" rel="noreferrer">Firefox</a> 標註為「Won't fix」(不修復)。限制是基於每個瀏覽器 + 網域,也就是說你可以針對 www.example1.com 網域在所有的分頁中開啟六個 SSE 連線,另一個網域 www.example2.com 也可以開啟六個(根據 <a href="https://stackoverflow.com/a/5326159/1905229">Stackoverflow</a>)。當使用 HTTP/2 時最大同時 <em>HTTP streams</em> 連線數是由伺服器和客戶端之間協調(預設 100)。</p>
</div>

<h2 id="從伺服器發送事件">從伺服器發送事件</h2>

<p>由伺服器端所發送的事件需要使用 <code>text/event-stream</code> 的 MIME 類型回應。每一個通知皆由一組文字組成並由一對換行結尾。如何處理事件串流的格式,請參考 <a href="#事件串流(event_stream)格式">Event stream format</a></p>

<p>下面是一個 {{Glossary("PHP")}} 範例:</p>

<pre class="brush: php notranslate">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);
}
</pre>

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

<div class="note">
<p><strong>備註:</strong> 你可以從下列的 Github 文章中找到包含本文所使用程式碼的完整範例 —— 參考 <a href="https://github.com/mdn/dom-examples/tree/master/server-sent-events">Simple SSE demo using PHP.</a></p>
</div>

<h2 id="錯誤處理">錯誤處理</h2>

<p>當錯誤發生時(譬如網路逾時或有關<a href="/zh-TW/docs/Web/HTTP/CORS">存取控制</a>的問題)會產生錯誤事件。你可以透過對 <code>EventSource</code> 物件實作 <code>onerror</code> 回呼的方式採取程式化的處理:</p>

<pre class="brush: js notranslate">evtSource.onerror = function(err) {
  console.error("EventSource failed:", err);
};
</pre>

<h2 id="關閉事件串流">關閉事件串流</h2>

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

<pre class="notranslate">evtSource.close();</pre>

<h2 id="事件串流(Event_Stream)格式">事件串流(Event Stream)格式</h2>

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

<div class="note"><strong>備註:</strong> 註解將有助於防止連線逾時;伺服器端可以定時發送註解以維持連線活著。</div>

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

<h3 id="欄位">欄位</h3>

<p>每隔訊息皆可以由下列的欄位組合而成,每個欄位以換行做為區隔:</p>

<dl>
 <dt><code>event</code></dt>
 <dd>事件的類型。如果有指定則在瀏覽器端會對該事件名稱的監聽器發布事件;網頁的原始碼必須使用 <code>addEventListener()</code> 來監聽已命名的事件。 <code>onmessage</code> 處理器只有在訊息沒有指定事件名稱時才會被呼叫。</dd>
 <dt><code>data</code></dt>
 <dd>訊息的資料欄位。當 EventSource 連續接收到多列以 <code>data:</code> 開頭的內容;<a href="http://www.w3.org/TR/eventsource/#dispatchMessage">它會串接這些內容</a>並為每一列插入一個換行字元。最後的換行會被移除。</dd>
 <dt><code>id</code></dt>
 <dd>{{domxref("EventSource")}} 物件的最新一個事件 ID 。</dd>
 <dt><code>retry</code></dt>
 <dd>當嘗試傳送事件時重新連線的時間。這個值必須是整數,單位是毫秒,作為重新連線的時間。若指定是非整數則這個欄位會被忽略。</dd>
</dl>

<p>除上述的幾個欄位,其他欄位均會被忽略。</p>

<div class="note"><strong>備註:</strong>如果某列的內容沒有包含冒號,則該列的內容都會被視為欄位名稱及空字串的欄位值。</div>

<h3 id="範例">範例</h3>

<h4 id="Data-only_訊息">Data-only 訊息</h4>

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

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

<pre class="notranslate">: this is a test stream

data: some text

data: another message
data: with two lines
</pre>

<h4 id="命名事件">命名事件</h4>

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

<pre class="notranslate">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."}
</pre>

<h4 id="混合及配對">混合及配對</h4>

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

<pre class="notranslate">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."}</pre>

<div>
<h3 id="EventSource"><code>EventSource</code></h3>

<div>


<p>{{Compat("api.EventSource")}}</p>
</div>
</div>