- 后端
- 网络
- 通信
【免费下载链接】undici
An HTTP/1.1 client, written from scratch for Node.js
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'事件的几种触发路径:
- 响应为中止的网络错误(
aborted)→ 关闭连接并派发error,不再重连; - 响应为普通网络错误→ 调用
#reconnect()进入重连流程(并派发error); - 响应状态码非 200,或
Content-Type的 essence 不是text/event-stream→ 关闭并派发error; - 连接中途流式传输失败(非正常中止)→ 关闭并派发
error(见 pipeline 回调)。
连接生命周期与重连机制
EventSource在每次建立连接时都会克隆原始请求再发起 fetch,而不是复用同一请求对象。源码注释(eventsource.js)解释了原因:fetch 会修改传入的请求(URL 列表、重定向次数、响应污染、跨源重定向时的头),复用会导致重连指向最后一次重定向目标,并在累计 20 次重定向后永久失败;因此cloneRequest()克隆请求、并单独拷贝urlList。所有重定向(301/308 永久重定向与 302/307 临时重定向)按相同方式处理。
重连流程(#reconnect 源码):
- 若
readyState为CLOSED,直接中止; - 将
readyState置为CONNECTING; - 派发
error事件; - 等待
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
相关推荐
Undici事件流处理:EventSource与Server-Sent Events实战
Undici事件流处理:EventSource与Server Sent Events实战 你是否还在为Node.js应用中的实时数据传输烦恼?长轮询效率低下,W
后端网络通信Oak框架中的Server-Sent Events(SSE)实现详解
Oak框架中的Server Sent Events SSE 实现详解 什么是Server Sent Events Server Sent Events 简称SS
后端GeoIP2 Java API完整指南:轻松实现IP地理位置查询的7个关键步骤
GeoIP2 Java API完整指南:轻松实现IP地理位置查询的7个关键步骤 GeoIP2 Java API是一个功能强大的开源库,专门用于在Java应用中实
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考