使用服务器发送事件

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2020 年 1 月⁩ 起,所有主流浏览器均已支持。

* 此特性的某些部分可能存在不同级别的支持。

开发使用 服务器发送事件 的 Web 应用程序很简单。您需要在服务器端编写一些代码来将事件流式传输到前端,但在处理传入事件的某些方面,客户端代码与 WebSocket 几乎相同。这是一个单向连接,因此您无法从客户端向服务器发送事件。

从服务器接收事件

服务器发送事件 API 包含在 EventSource 接口中。

创建 EventSource 实例

要打开与服务器的连接以开始从中接收事件,请使用生成事件的脚本的 URL 创建一个新的 EventSource 对象。例如:

js
const evtSource = new EventSource("sse-demo.php");

如果事件生成器脚本托管在不同的域上,则应使用 URL 和选项字典创建一个新的 EventSource 对象。例如,假设客户端脚本位于 example.com

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

监听 message 事件

从服务器发送的、没有 event 字段的消息将作为 message 事件接收。要接收消息事件,请为 message 事件附加一个处理程序:

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

  newElement.textContent = `message: ${event.data}`;
  eventList.appendChild(newElement);
};

此代码监听传入的消息事件,并将消息文本追加到文档 HTML 中的列表中。

监听自定义事件

服务器发送的、具有 event 字段的消息将作为在 event 中命名的事件接收。例如:

js
evtSource.addEventListener("ping", (event) => {
  const newElement = document.createElement("li");
  const eventList = document.getElementById("list");
  const time = JSON.parse(event.data).time;
  newElement.textContent = `ping at ${time}`;
  eventList.appendChild(newElement);
});

每当服务器发送 event 字段设置为 ping 的消息时,都会调用此代码;然后它会解析 data 字段中的 JSON 并输出该信息。

警告:不通过 HTTP/2 使用时,SSE 会受到最大打开连接数的限制,这在打开多个选项卡时可能会特别痛苦,因为限制是每个浏览器并且设置为一个非常小的数字(6)。这个问题已在 ChromeFirefox 中被标记为“不会修复”。此限制是每个浏览器 + 域,这意味着您可以打开 6 个到 www.example1.com 的 SSE 连接,以及另外 6 个到 www.example2.com 的 SSE 连接(根据 Stack Overflow)。使用 HTTP/2 时,同时HTTP 流的最大数量在服务器和客户端之间协商(默认为 100)。

从服务器发送事件

发送事件的服务器端脚本需要使用 text/event-stream MIME 类型进行响应。每个通知都作为一个文本块发送,并以一对换行符终止。有关事件流格式的详细信息,请参阅 事件流格式

我们在此示例中使用的 PHP 代码如下:

php
date_default_timezone_set("America/New_York");
header("X-Accel-Buffering: no");
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");

$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);
  }

  if (ob_get_contents()) {
      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 上找到使用本文档所示代码的完整示例 — 请参阅 使用 PHP 的简单 SSE 演示

错误处理

如果服务器响应了 error 键(例如,JSON.parse(event.data.error) 或发生其他问题(例如网络超时或与 访问控制相关的问题)),则会生成一个错误事件。您可以通过实现 EventSource 对象上的 onerror 回调来以编程方式处理此问题。

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

关闭事件流

默认情况下,如果客户端和服务器之间的连接关闭,连接将重新启动。通过 .close() 方法终止连接。

js
evtSource.close();

事件流格式

事件流是一个简单的文本数据流,必须使用 UTF-8 进行编码。事件流中的消息由一对换行符分隔。行首的冒号本质上是注释,会被忽略。

注意:注释行可用于防止连接超时;服务器可以定期发送注释以保持连接活跃。

每条消息由一行或多行文本组成,列出该消息的字段。每个字段由字段名、冒号和字段值的文本数据组成。

字段

每条收到的消息都包含以下字段的某种组合,每行一个:

event

一个字符串,标识事件的类型。如果指定了此字段,则会在浏览器上为指定事件名的监听器分派一个事件;网站源代码应使用 addEventListener() 来监听命名事件。如果没有为消息指定事件名,则会调用 onmessage 处理程序。

data

消息的数据字段。当 EventSource 收到多个以 data: 开头的连续行时,它会将它们连接起来,并在它们之间插入一个换行符。尾随的换行符会被删除。

id

事件 ID,用于设置 EventSource 对象的最后事件 ID 值。

retry

重新连接时间。如果与服务器的连接丢失,浏览器将在尝试重新连接之前等待指定的时间。这必须是一个整数,指定以毫秒为单位的重新连接时间。如果指定了非整数值,则忽略该字段。

所有其他字段名都将被忽略。

注意:如果一行不包含冒号,则整行被视为字段名,其值为一个空字符串。

示例

仅数据消息

在以下示例中,发送了三条消息。第一条只是一个注释,因为它以冒号开头。如前所述,这可以作为保持活动机制,如果消息可能不会定期发送。

第二条消息包含一个值为“some text”的数据字段。第三条消息包含一个值为“another message\nwith two lines”的数据字段。请注意值中的换行特殊字符。

bash
: this is a test stream

data: some text

data: another message
data: with two lines

命名事件

此示例发送命名事件。每个事件都有一个由 event 字段指定的事件名,以及一个 data 字段,其值为适当的 JSON 字符串,包含客户端处理事件所需的数据。当然,data 字段可以是任何字符串数据;不一定是 JSON。

bash
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."}

混合搭配

您不必只使用未命名的消息或类型化事件;您可以在单个事件流中混合使用它们。

bash
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."}

浏览器兼容性