☰
undici EventSource 使用与原理详解:在 Node.js 中实现 Server-Sent Events
2026/9/27 21:51:56 网站建设 项目流程
  • 后端
  • 网络
  • 通信

【免费下载链接】undici

An HTTP/1.1 client, written from scratch for Node.js

项目地址:https://gitcode.com/gh_mirrors/un/undici
点击查看免费下载

EventSource是 undici 提供的 WHATWG 规范 并结合仓库源码,从构造参数、实例属性、事件模型、重连机制到流式解析原理,带你完整掌握在 undici 中消费 SSE 流的能力。

概述:WHATWG 标准的 Server-Sent Events 客户端

EventSource接口在 undici 中是一个EventTarget的子类(见 lib/web/eventsource/eventsource.js),它打开一条到服务器的持久 HTTP 连接,服务端以text/event-stream内容类型持续推送事件,客户端收到后即时派发事件,连接不会被关闭。它是浏览器原生EventSource的 Node.js 等价实现,遵循 WHATWG Server-Sent Events 处理模型。

需要注意两点:

  • 稳定性:该接口标记为Stability: 1 - Experimental(实验性),从 v6.5.0 开始加入,接口可能在后续版本中发生变化。
  • 首次构造警告:第一次构造EventSource时会发出一次性ExperimentalWarning,其code为'UNDICI-ES'(源码见 eventsource.js 构造函数)。
  • 全局安装:该接口还会被安装到globalThis上,即globalThis.EventSource可直接使用(安装列表见 lib/global.js)。

最小用法如下:

import { EventSource } from 'undici' const eventSource = new EventSource('http://localhost:3000') eventSource.onmessage = (event) => { console.log(event.data) }

构造函数:new EventSource(url[, eventSourceInitDict])

参数说明

参数类型说明
url{string|URL}事件流地址。相对 URL 会基于当前环境的基础 URL 解析。
eventSourceInitDict{Object}(可选)初始化字典,包含以下成员:

eventSourceInitDict支持以下选项:

  • withCredentials{boolean}:为true时,请求的 credentials 模式设为include,CORS attribute 状态设为use-credentials;否则 credentials 模式为same-origin。默认:false。

  • dispatcher{Dispatcher}:底层请求使用的 dispatcher。默认:全局 dispatcher。

    Stability: 0 - Deprecated(已弃用),请改用node.dispatcher。

  • node{Object}:undici 对标准EventSourceInit字典的扩展:

    • dispatcher{Dispatcher}:底层请求使用的 dispatcher。默认:全局 dispatcher。
    • reconnectionTime{number}:断线后等待重新建立连接的时间(毫秒)。服务端可通过retry字段覆盖此值。默认:3000。

注意:dispatcher选项标记为弃用(Stability 0),推荐使用node.dispatcher。从源码看,构造函数会优先取node.dispatcher,其次才是顶层dispatcher(见 eventsource.js);reconnectionTime的默认值3000与 Chrome 一致(Deno 使用 5000),定义在 源码常量。

构造时的请求行为

创建EventSource实例后会立即开始连接url。请求以以下特征发出(对应 构造函数源码):

  • 请求头Accept: text/event-stream;
  • cache 模式为no-store;
  • initiator 类型为other;
  • 请求由createPotentialCORSRequest构造:模式为cors,credentials 模式依据withCredentials在same-origin与include之间切换(见 lib/web/eventsource/util.js)。

如果url无法解析,会抛出SyntaxError类型的DOMException。仓库测试 test/eventsource/eventsource.js 验证了不传url抛TypeError、非法 URL 抛带Invalid URL消息的 DOMException。

带withCredentials的示例:

import { EventSource } from 'undici' const eventSource = new EventSource('http://localhost:3000', { withCredentials: true })

自定义 Dispatcher 注入请求头

可以通过自定义Dispatcher控制底层请求,例如在派发时追加请求头:

import { EventSource, Agent } from 'undici' class CustomHeaderAgent extends Agent { dispatch (opts) { opts.headers['x-custom-header'] = 'hello world' return super.dispatch(...arguments) } } const eventSource = new EventSource('http://localhost:3000', { node: { dispatcher: new CustomHeaderAgent() } })

在 构造函数源码 中,this.#dispatcher被直接保存并在每次#connect()时传给底层fetching(),因此自定义 dispatcher 会作用于每次连接(含重连)。

实例属性

eventSource.readyState

{number},只读,表示连接状态,取值为以下常量之一:

  • EventSource.CONNECTING(0)——连接尚未建立,或连接关闭后正在重新建立;
  • EventSource.OPEN(1)——连接已打开,正在按收到顺序派发事件;
  • EventSource.CLOSED(2)——连接未打开,且不再尝试重连。

eventSource.url

{string},只读,事件流地址,为基于环境基础 URL 解析后的最终 URL(源码中保存的是new URL(url, baseUrl).href,见 eventsource.js)。

eventSource.withCredentials

{boolean},只读,表示是否以 CORS credentials 方式实例化(true)或否(false,默认值),反映构造时传入的withCredentials选项。

事件处理器属性

属性类型默认值触发时机
eventSource.onopen{Function|null}null派发'open'事件时
eventSource.onmessage{Function|null}null派发'message'事件时
eventSource.onerror{Function|null}null派发'error'事件时

赋值函数会注册为对应事件的处理器;赋值为null则移除当前处理器。从源码实现看,这些 setter 会先removeEventListener旧的处理器,再通过webidl.converters.EventHandlerNonNull转换后addEventListener注册新处理器(见 eventsource.js)。

静态常量:CONNECTING/OPEN/CLOSED

  • EventSource.CONNECTING:数值常量0;
  • EventSource.OPEN:数值常量1;
  • EventSource.CLOSED:数值常量2。

它们被定义为只读且不可写的属性,同时存在于EventSource构造函数与实例原型上(通过Object.defineProperties以writable: false定义,见 eventsource.js)。

方法:eventSource.close()

关闭连接(如有),中止底层请求,并将readyState置为CLOSED。关闭后EventSource不会再尝试重连;对已关闭的实例再次调用close()无任何效果。

源码实现(eventsource.js)为:将readyState置为CLOSED,然后调用#controller.abort()中止 fetch,并清空#request引用。另外,#connect()与#reconnect()的入口都会检查readyState === CLOSED并直接返回,从而保证关闭后不再建立连接。

事件

Event:'open'

连接建立、readyState变为OPEN时触发,监听器收到一个Event对象。

import { EventSource } from 'undici' const eventSource = new EventSource('http://localhost:3000') eventSource.addEventListener('open', () => { console.log('connection opened') })

Event:'message'

当收到没有显式event字段的消息时触发(v6.15.0 加入)。监听器收到一个MessageEvent,其data、lastEventId、origin属性由服务端事件填充。命名事件(带event字段的事件)会在自己的类型名下派发,必须通过addEventListener()订阅。

下面是一个完整的服务端推送 + 客户端消费示例:

import { createServer } from 'node:http' import { EventSource } from 'undici' const server = createServer((request, response) => { response.writeHead(200, { 'content-type': 'text/event-stream', 'cache-control': 'no-cache', connection: 'keep-alive' }) response.write('event: ping\n') response.write('data: connected\n\n') const interval = setInterval(() => { response.write(`data: ${Date.now()}\n\n`) }, 1000) request.on('close', () => clearInterval(interval)) }) server.listen(3000, () => { const eventSource = new EventSource('http://localhost:3000') // Named event, delivered under its own type. eventSource.addEventListener('ping', (event) => { console.log('ping:', event.data) }) // Unnamed event, delivered as 'message'. eventSource.onmessage = (event) => { console.log('message:', event.data) } })

注意:示例中服务端发送了event: ping命名事件与若干无event字段的默认事件;命名事件只能通过addEventListener('ping', ...)收到,默认事件则统一进入onmessage/'message'。这正是 WHATWG SSE 处理模型中的"命名事件按自身类型派发"规则。

Event:'error'

连接失败或中断时触发,监听器收到一个Event对象。当失败可恢复时,EventSource回到CONNECTING状态并在重连时间后重试;当失败不可恢复时,EventSource转为CLOSED且不再重连。

v7.11.0 行为变更:网络错误后不再重新建立连接,而是直接关闭EventSource。

import { EventSource } from 'undici' const eventSource = new EventSource('http://localhost:3000') eventSource.onerror = () => { if (eventSource.readyState === EventSource.CLOSED) { console.log('connection closed') } else { console.log('reconnecting') } }

结合 processResponse 源码 可以看到'error'事件的几种触发路径:

  1. 响应为中止的网络错误(aborted)→ 关闭连接并派发error,不再重连;
  2. 响应为普通网络错误→ 调用#reconnect()进入重连流程(并派发error);
  3. 响应状态码非 200,或Content-Type的 essence 不是text/event-stream→ 关闭并派发error;
  4. 连接中途流式传输失败(非正常中止)→ 关闭并派发error(见 pipeline 回调)。

连接生命周期与重连机制

EventSource在每次建立连接时都会克隆原始请求再发起 fetch,而不是复用同一请求对象。源码注释(eventsource.js)解释了原因:fetch 会修改传入的请求(URL 列表、重定向次数、响应污染、跨源重定向时的头),复用会导致重连指向最后一次重定向目标,并在累计 20 次重定向后永久失败;因此cloneRequest()克隆请求、并单独拷贝urlList。所有重定向(301/308 永久重定向与 302/307 临时重定向)按相同方式处理。

重连流程(#reconnect 源码):

  1. 若readyState为CLOSED,直接中止;
  2. 将readyState置为CONNECTING;
  3. 派发error事件;
  4. 等待reconnectionTime毫秒后,若readyState仍为CONNECTING:
    • 若本地记录有lastEventId,则在请求头中设置Last-Event-ID(先删除旧值再设置,且会校验值合法性,见 isValidHeaderValue 检查);
    • 重新调用#connect()。

两个值得注意的实现细节:

  • 定时器使用setTimeout(...).unref(),因此仅剩重连定时器在跑时不会阻止 Node.js 进程退出;
  • 重连时间会与maxReconnectionTime = 2 ** 31 - 1取最小值。源码注释指出,超过该值的延迟(包括Infinity)会让 Node.js 在 1ms 后触发定时器,从而把"很长"的重连时间变成重连风暴(见 eventsource.js)。

retry字段对重连时间的覆盖

服务端可在事件流中发送retry: <毫秒>字段,客户端解析后会更新当前reconnectionTime。这在 EventSourceStream.processEvent 中实现:仅当retry值全部为 ASCII 数字时才生效(parseInt解析),非法值被忽略。

Last-Event-ID断点续传

服务端可在事件中发送id: <字符串>字段,客户端将其记录为lastEventId(校验规则是不含U+0000NULL 字符,见 util.js)。重连时,该值会通过Last-Event-ID请求头发回服务端,实现断点续传(例如事件流中途断开后,服务端可依据该头从断点继续推送)。若某条事件流中携带的id含 NULL 字符,该字段会被忽略。

流式解析原理:EventSourceStream

收到合法响应后,undici 将响应体通过pipeline接入EventSourceStream(一个 object-mode 的Transform流,见 eventsource-stream.js),逐字节解析text/event-stream格式并把解析结果 push 为MessageEvent。

解析器实现了 WHATWG 规范的完整规则(parseLine 实现):

  • 空行:结束当前事件,若事件有data则派发(否则丢弃);
  • 以:开头的行:注释行,忽略;
  • data:字段:累计事件数据;同一事件内多行data以\n连接(见 源码);仅有data字段的事件派发为'message'类型,携带data、lastEventId、origin;
  • event:字段:为事件命名,派发时使用该名称作为事件类型(processEvent);
  • id:字段:更新lastEventId;
  • retry:字段:更新重连时间;
  • BOM 处理:流开头若为 UTF-8 BOM(EF BB BF)会自动剥离(handleBOM);
  • 行结束符:兼容LF、CR与CRLF三种行结束方式。

每个事件的MessageEvent还带有origin,指向响应最终 URL 的 origin(跨源重定向后 origin 会更新为最终 origin,见 eventsource.js)。

配置 EventSource 事件大小上限

EventSource 相关的限制可以在 dispatcher 上通过eventSource选项配置。具体到Client(详见 Client 构造参数):

  • eventSource.maxEventSize{number}:EventSource 消息允许的最大事件大小(字节)。设为0可禁用限制。默认:buffer.kStringMaxLength。

该选项在 DispatcherBase 中被接收并暴露为eventSourceOptions,随后在建立连接时传入EventSourceStream(见 eventsource.js)。当单个事件累计数据超过上限时,解析器会抛出EventSource message size exceeded错误(错误对象带aborted = false,见 eventsource-stream.js),并导致连接关闭、派发error事件。

进阶场景:将 EventSource 与 Dispatcher 生命周期配合

由于EventSource可注入任意Dispatcher(推荐通过node.dispatcher),你可以:

  • 复用已配置的Agent/Client(例如配置了代理、TLS、连接池参数的实例)来承载事件流请求,避免使用全局默认 dispatcher;
  • 通过自定义Agent.dispatch()拦截请求(如追加鉴权头、统计埋点),正如上文CustomHeaderAgent示例;
  • 通过Client的eventSource.maxEventSize收紧或放宽事件大小限制。

小结

undici 的EventSource提供了一个与浏览器行为一致的 SSE 客户端:自动重连(含retry字段覆盖与Last-Event-ID断点续传)、完整的事件派发模型(open/message/error与命名事件)、兼容各种行结束符与 BOM 的流式解析器,以及可通过 dispatcher 定制请求与事件大小限制的能力。其行为细节均可对照 EventSource 官方文档、实现源码 与 测试用例 进一步验证与探索。

  • 后端
  • 网络
  • 通信

【免费下载链接】undici

An HTTP/1.1 client, written from scratch for Node.js

项目地址:https://gitcode.com/gh_mirrors/un/undici
点击查看免费下载
上一篇:免费离线跨平台:drawio-desktop 流程图桌面工具完整指南
下一篇:RevokeMsgPatcher 完整实测:PC 微信 QQ 防撤回补丁 4 分钟装完,撤回彻底失效

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询