伺服器傳送事件 規範說明了一個內建類別 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}`);
});
完整範例
以下是一個伺服器,它會傳送訊息 1
、2
、3
,然後是 bye
,並中斷連線。
然後瀏覽器會自動重新連線。
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:
會放在最後。
留言
<code>
標籤,要插入多行程式碼,請用<pre>
標籤將它們包起來,要插入超過 10 行的程式碼 - 請使用沙盒 (plnkr、jsbin、codepen…)