首页
/ engine.io-client 版本演进与源码解析:Socket.IO 底层传输客户端的升级探测、Tree-shaking 与传输层兜底

engine.io-client 版本演进与源码解析:Socket.IO 底层传输客户端的升级探测、Tree-shaking 与传输层兜底

2026-09-04 22:39:51作者:魏侃纯Zoe

engine.io-client 是 socket.io monorepo 中为 Socket.IO 提供底层双向通信能力的传输客户端,负责 HTTP 长轮询、WebSocket 与 WebTransport 三种传输层的选择、握手、心跳与升级。本文以 packages/engine.io-client/CHANGELOG.md 为主线,梳理其从 3.x 到 6.6.x 的版本演进脉络,并结合 lib/socket.tslib/transport.ts 等源码,讲透自定义传输实现、传输层 Tree-shaking、tryAllTransports 兜底、WebTransport 支持、close 事件详情等关键能力在 6.x 中的落地方式与底层实现。

engine.io-client 在 socket.io 生态中的位置

packages/ 目录中,各包之间存在清晰的依赖分层:socket.io-client(应用层客户端)依赖 engine.io-client(传输层客户端),后者依赖 engine.io-parser(协议编解码),对应服务端则是 engine.io 包。engine.io-clientpackage.json 声明了当前版本 6.6.6 及其运行时依赖:

  • @socket.io/component-emitter ~3.1.0:事件发射器基类
  • debug ~4.4.1:调试日志(2025-12-23 的 6.6.4 将其从 ~4.3.1 升至 ~4.4.1
  • engine.io-parser ~5.2.1:Engine.IO 协议编解码
  • ws ~8.21.0:Node.js 侧 WebSocket 实现(6.6.6 为修复 CVE-2026-48779 而从 ~8.20.1 升级)
  • xmlhttprequest-ssl ~2.1.1:Node.js 侧长轮询所需的 XHR 模拟

该包同时提供 CommonJS(./build/cjs/index.js)、ESM(./build/esm/index.js)以及独立的 /debug 导出,并通过 exports 字段在 Node.js 与浏览器间选择不同的入口;浏览器构建时还会借助 browser 字段把 Node 专属模块(如 websocket.node.jspolling-xhr.node.js)替换为对应的浏览器实现。

版本总览与 Bundle Size 演进

CHANGELOG 以表格形式记录了每个版本的发布日期、所依赖的 ws 版本与 UMD min+gzip 后的 Bundle 体积。主分支版本如下(" 表示与上一行相同):

Version Release date ws version Bundle size (UMD min+gzip)
6.6.6 June 2026 ~8.21.0 8.9 KB
6.6.5 May 2026 ~8.20.1 8.7 KB
6.6.4 December 2025 ~8.18.3 8.7 KB
6.6.3 January 2025 " 8.7 KB
6.6.2 October 2024 " 8.7 KB
6.6.1 September 2024 " 8.7 KB
6.6.0 June 2024 ~8.17.1 8.6 KB
6.5.3 November 2023 " 8.8 KB
6.5.2 August 2023 " 8.8 KB
6.5.1 June 2023 " 8.4 KB
6.5.0 June 2023 " 7.8 KB
6.4.0 February 2023 " 7.8 KB
6.3.1 February 2023 " 7.8 KB
6.3.0 January 2023 ~8.11.0 8.0 KB
6.2.3 October 2022 " 7.8 KB
6.2.2 May 2022 " 7.8 KB
6.2.1 April 2022 " 7.8 KB
6.2.0 April 2022 " 7.8 KB
6.1.1 November 2021 " 7.4 KB
6.1.0 November 2021 " 7.4 KB
6.0.2 October 2021 " 7.4 KB
6.0.1 October 2021 " 7.4 KB
6.0.0 October 2021 ~8.2.3 7.5 KB
5.2.0 August 2021 " 9.4 KB
5.1.2 June 2021 " 9.3 KB
5.1.1 May 2021 " 9.2 KB
5.1.0 May 2021 " 9.2 KB
5.0.1 March 2021 " 9.2 KB
5.0.0 March 2021 " 9.3 KB
4.1.2 February 2021 " 9.2 KB
4.1.1 February 2021 " 9.1 KB
4.1.0 January 2021 ~7.4.2 9.1 KB

除主分支外,CHANGELOG 还维护了若干遗留分支的版本记录:

分支 版本 Release date ws version
6.5.x 6.5.4 June 2024 ~8.17.1
3.5.x 3.5.4 June 2024 ~7.5.10
3.5.x 3.5.3 September 2022 ~7.4.2
3.5.x 3.5.2 May 2021 "
3.5.x 3.5.1 March 2021 ~6.1.0
6.0.x 6.0.3 November 2021 "
4.1.x 4.1.4 May 2021 "

从体积数据可以看出两条演进线索:4.x → 5.x 期间 Bundle 约 9.2 KB,5.2.0 后降至 9.4 KB 上下;6.0.0 重构构建管线后降至 7.5 KB 区间;6.5.0 引入 WebTransport 后小幅回升,6.6.x 稳定在 8.6~8.9 KB。这一数据由 npm run bundle-size(即 support/bundle-size.js)测量并回写到 CHANGELOG。

4.0.0 与 6.0.0:两次重大重构

4.0.0(2020-09-10):ping-pong 机制反转

4.0.0 引入了一项破坏性变更:反转 ping-pong 心跳机制。此前客户端向服务端发 ping 并等待 pong,4.0.0 起改为服务端发 ping、客户端回 pong。CHANGELOG 明确警告:v3.x 客户端将无法再连接到 v4 服务端——它们会发送 ping 包并因始终等不到 pong 包而超时。这一变化在源码中有直接体现:lib/socket.ts_onPacket 方法中,收到 ping 类型包即自动回复 pong 并重置超时。后续 5.1.2 又修复了"收到服务端 ping 时先 emit ping 再回包"的时序细节,保证应用层监听到的 ping/pong 事件与服务端实际行为一致。

其余 4.x 相关变更还包括:4.0.3 针对 React Native 环境收敛了 withCredentials 默认值并排除 localAddress 选项;4.1.0 补全了透传给 ws 包的选项;4.1.1 移除了 Bundle 中的 process polyfill;4.1.2 在 beforeunload 钩子中静默关闭传输,避免页面关闭时服务端收到多余断开事件。

6.0.0(2021-10-08):TypeScript 化与构建重构

6.0.0 包含三项重要变更(均与仓库现状吻合):

  1. 代码库迁移到 TypeScript——当前 lib/ 下全部为 .ts 源文件;
  2. 以 rollup 取代 webpack 生成 Bundle——对应 support/rollup.config.umd.jssupport/rollup.config.esm.js
  3. 移除对远古浏览器(如 IE8)的支持代码

由此产生了三套独立构建产物:build/cjs(CommonJS)、build/esm(无 debug 的 ESM)、build/esm-debug(含 debug 的 ESM),以及三个浏览器 Bundle:未压缩 UMD、压缩 UMD、ESM 压缩版。值得注意的是,通信协议本身没有升级,因此 v5 客户端可以连接 v6 服务端,反之亦然——协议规范见 docs/engine.io-protocol/v4-current.md

BREAKING CHANGES 方面:enableXDR 选项被移除,jsonpforceJSONP 选项被移除。6.0.1/6.0.2 修复了 vite 场景下的构建问题;6.0.3 则把部分修复回移到 6.0.x 维护分支,供最新 socket.io-client 使用。

5.x:面向 Node.js 长连接与页面生命周期

  • 5.0.0(因服务端破坏性变更而 major bump):新增 autoUnref 选项——心跳定时器调用 unref(),避免长时间空闲时客户端阻止 Node.js 进程退出;同时开始监听浏览器的 offline 事件,在网络断开时主动关闭连接。当前源码中,offline 监听被放在模块顶层(lib/socket.ts),用单一全局监听器转发给所有 socket 实例——注释说明了原因:ServiceWorker 中的 offline 监听器必须在脚本初始求值时注册。
  • 5.1.0:新增 closeOnBeforeunload 选项,在页面关闭/刷新时静默关闭传输,统一 Firefox 与 Chrome 的行为;6.5.1 又把它默认值从 true 改回 false,源码默认值可见 lib/socket.tscloseOnBeforeunload: false
  • 5.2.0:新增 useNativeTimers 选项,允许在 mock 时钟(如测试环境的假定时器)覆盖全局 setTimeout 时仍使用原生定时器,保证重连可用;installTimerFunctionslib/util.ts)据此为实例选择定时器实现。

6.6.0:自定义传输实现与 Tree-shaking

6.6.0(2024-06-21)是 6.x 中最重要的功能版本,围绕"传输层可插拔"提供了三项能力。

1. transports 选项接受实现类数组

transports 选项除了接受 ["polling", "websocket", "webtransport"] 这样的字符串数组外,现在还可以直接传入传输实现类:

import { Socket, XHR, WebSocket } from "engine.io-client";

const socket = new Socket({
  transports: [XHR, WebSocket]
});

官方提供的实现类清单如下(类名与 lib/index.ts 中的导出完全对应,注意 NodeXHRNodeWebSocket 是 Node 侧 XHR/WS 的别名导出):

Transport Description
Fetch 基于内置 fetch() 的 HTTP 长轮询
NodeXHR 基于 xmlhttprequest-ssl 包提供 XHR 对象的 HTTP 长轮询
XHR 基于内置 XMLHttpRequest 对象的 HTTP 长轮询
NodeWebSocket 基于 ws 包提供 WebSocket 对象的传输
WebSocket 基于内置 WebSocket 对象的传输
WebTransport 基于内置 WebTransport 对象的传输

各实现的平台可用性:

Transport browser Node.js Deno Bun
Fetch 支持 支持 (1) 支持 支持
NodeXHR 支持 支持 支持
XHR 支持
NodeWebSocket 支持 支持 支持
WebSocket 支持 支持 (2) 支持 支持
WebTransport 支持 支持

(1) 自 Node.js v18.0.0 起可用 fetch;(2) 自 Node.js v21.0.0 起内置 WebSocket

从源码结构看,实现类的来源是 lib/transports/ 目录:polling-fetch.tspolling-xhr.node.tspolling-xhr.tswebsocket.node.tswebsocket.tswebtransport.ts。当使用字符串形式时,Socket 构造函数会把名称映射为默认实现(lib/socket.ts):

o.transports = (o.transports || ["polling", "websocket", "webtransport"])
  .map((transportName) => DEFAULT_TRANSPORTS[transportName])
  .filter((t) => !!t);

其中 DEFAULT_TRANSPORTS 的定义因运行环境而异:Node.js 入口(lib/transports/index.ts)映射为 websocket: WS(ws 包)、webtransport: WTpolling: XHR(xmlhttprequest-ssl);浏览器入口则通过 package.jsonbrowser 字段在构建期把 .node.js 模块替换为浏览器版本,从而实现"一份源码、两端产物"。

2. 传输层 Tree-shaking:SocketWithoutUpgrade / SocketWithUpgrade

同一版本还引入了把未使用传输代码从最终 Bundle 中剔除(tree-shaking)的能力:

import { SocketWithoutUpgrade, WebSocket } from "engine.io-client";

const socket = new SocketWithoutUpgrade({
  transports: [WebSocket]
});

此时与 HTTP 长轮询和 WebTransport 相关的代码将被排除出 Bundle。这一设计的落点在 lib/socket.ts 的类结构上:SocketWithoutUpgrade 是纯基类,自身不内置任何传输,其 JSDoc 明确写着 "In order to allow tree-shaking, there are no transports included, that's why the transports option is mandatory";SocketWithUpgrade 继承它并加入升级探测逻辑;Socket 再继承 SocketWithUpgrade 并补上字符串名称到默认实现的映射。三者都从 lib/index.ts 导出,打包器即可按实际引用裁剪。

3. tryAllTransports:逐个探测所有传输

设置 tryAllTransports: true 后,如果第一个传输(通常是 HTTP 长轮询)失败,客户端会继续尝试下一个传输:

import { Socket } from "engine.io-client";

const socket = new Socket({
  tryAllTransports: true
});

该选项适用于两类场景:服务端禁用了长轮询、或 CORS 校验失败;以及把 WebSocket 放在 transports: ["websocket", "polling"] 首位做探测的场景。其代价是失败时整体连接耗时更长——CHANGELOG 提到有 WebSocket 连接错误需要数秒才能被发现的报告(这也是默认先试长轮询的原因),所以该选项默认 false

源码层面的实现非常直观,位于 _onErrorlib/socket.ts):

if (
  this.opts.tryAllTransports &&
  this.transports.length > 1 &&
  this.readyState === "opening"
) {
  this.transports.shift();
  return this._open();
}

即只在 opening 状态、且队列里还有其他传输时才 shift() 换下一个并重新 _open();否则会 emit error 并以 transport error 关闭连接。选项声明见 lib/socket.tstryAllTransports?: boolean@default false)。

4. 同版本其他修复

6.6.0 还包含两项 Bug Fix:为 cache-busting 字符串生成器加入随机性(降低多客户端命中同一缓存的概率);以及修复 Node.js 环境下 WebSocket 传输的 Cookie 管理问题。

6.5.0:WebTransport 与 Node.js 客户端 Cookie 管理

WebTransport 支持

6.5.0(2023-06-16)让 Engine.IO 客户端可以把 WebTransport 作为底层传输。WebTransport 是基于 HTTP/3 协议的双向传输 Web API,用于 Web 客户端与 HTTP/3 服务端之间的双向通信。实现位于 lib/transports/webtransport.ts,测试则独立于 test/webtransport.mjs

对 Node.js 客户端:在 Node.js 原生支持 WebTransport 之前,可以借助 @fails-components/webtransport 包注入全局对象:

import { WebTransport } from "@fails-components/webtransport";

global.WebTransport = WebTransport;

后续版本针对 WebTransport 做了持续打磨:6.5.1 修复了连接被异常中断(abruptly closed)时的处理,并把 closeOnBeforeunload 默认值改回 false;6.5.2 补上了正确的帧格式(framing)并使其遵守 binaryType 属性。仓库中的 examples/webtransport/ 提供了端到端示例(含证书生成脚本 examples/webtransport/generate_cert.shindex.js 服务端)。

从源码结构看,WebTransport 在升级探测中还有特殊地位:SocketWithUpgrade._probelib/socket.ts)中,若升级队列包含 webtransport 而当前探测的不是它,会以 200ms 延迟打开当前探测传输——注释写明 "favor WebTransport",即给 WebTransport 优先建连的窗口。

Node.js 客户端 Cookie 管理

6.5.0 起,Node.js 客户端在 withCredentials: true 时会把 Cookie 带在 HTTP 请求里,配合基于 Cookie 的 sticky session 更方便:

import { Socket } from "engine.io-client";

const socket = new Socket("https://example.com", {
  withCredentials: true
});

对应源码:构造函数中 if (this.opts.withCredentials) { this._cookieJar = createCookieJar(); }lib/socket.ts),CookieJar 由 lib/globals.node.ts 提供,仅存在于 Node.js 侧。6.6.0 中 "fix cookie management with WebSocket (Node.js only)" 进一步修复了 WebSocket 升级路径下 Cookie 的处理。

6.2.0:close 事件详情与 maxPayload 写缓冲切片

close 事件携带 reason 与 details

6.2.0(2022-04-17)让 close 事件带上额外调试信息。例如 HTTP 长轮询模式下 payload 超过服务端 maxHttpBufferSize 时:

socket.on("close", (reason, details) => {
  console.log(reason); // "transport error"

  // in that case, details is an error object
  console.log(details.message); // "xhr post error"
  console.log(details.description); // 413 (the HTTP status of the response)

  // details.context refers to the XMLHttpRequest object
  console.log(details.context.status); // 413
  console.log(details.context.responseText); // ""
});

注意:错误对象在此之前就已包含在事件中,为向后兼容而保留。类型上,lib/transport.ts 定义了 TransportError(含 descriptioncontext 字段)与 CloseDetails 接口;socket 层的 close 保留事件签名为 (reason: string, description?: CloseDetails | Error)lib/socket.ts)。reason 的取值可参考 _onClose 的调用点:"ping timeout""transport close""transport error""forced close" 等。

按 maxPayload 切片写缓冲

同版本起,服务端握手数据中新增 maxPayload 字段,客户端据此决定一次最多发多少包以不超过 maxHttpBufferSize。源码见 onHandshake 中的 this._maxPayload = data.maxPayload,以及 _getWritablePackets:仅在轮询传输且写缓冲多于 1 包时逐包累计字节长度,一旦超过 maxPayload 就只发送前缀部分,剩余包留到下一次 drain 后的 flush()。这直接避免了长轮询 POST 因超包体限制而收到 413 的问题。

6.3.x ~ 6.5.3:URL 处理与工程化修复

  • 6.3.0(2023-01-10):新增 addTrailingSlash 选项。此前请求路径默认追加尾部斜杠,现在可以禁用:
import { Socket } from "engine.io-client";

const socket = new Socket("https://example.com", {
  addTrailingSlash: false
});

此时请求 URL 为 https://example.com/engine.io 而非 https://example.com/engine.io/。实现位于构造函数(lib/socket.ts):path.replace(/\/$/, "") + (addTrailingSlash ? "/" : ""),默认值 addTrailingSlash: true。同版本还修复了含 @ 字符的相对 URL 解析(lib/contrib/parseuri.ts)、显式 setTimeout 上下文问题;6.3.1 修复类型定义中泄漏浏览器专属类型的问题。

  • 6.4.0(2023-02-06):minor 版本号跟随服务端变更而提升,客户端无实质变化。
  • 6.2.3 / 6.5.3:清理 beforeunload 监听器;增加 URL 最大长度限制;改进 Node 16 模块解析兼容性。
  • 6.2.2:简化 WebSocket 可用性检测——原先 "__initialize" in WebSocket 的探测为已弃用的 flashsocket 传输而设,在较新 webpack 版本下会误判为 true;同时改用 globalThis shim 的具名导出,解决浏览器字段下异步加载时默认导出异常的问题。
  • 6.5.2/6.5.3:见上文 WebTransport 相关修复。

6.6.1 ~ 6.6.6:近期稳定性与安全修复

  • 6.6.1(2024-09-21):把 offline 事件监听移到最前;仅当监听器存在时才移除;不在已过期(expired)的连接上发包。另有一项性能改进:不再在每收到一个包时重置心跳定时器——超时定时器只在处理 ping/pong 时重建(见 _resetPingTimeout),并在 _pingTimeoutTime 上记录预期到期时间,配合 _hasPingExpired() 应对定时器被节流(如笔记本休眠、手机锁屏)的场景。
  • 6.6.2(2024-10-23):从 .d.ts 中移除 ws 类型引用(避免浏览器用户被迫安装 ws 的类型定义);防止使用 Node.js 内置 WebSocket 时进入无限循环。
  • 6.6.3(2025-01-23):修正对 ws 包的消费方式(correctly consume the ws package),保证打包与运行时行为一致。
  • 6.6.4(2025-12-23):正确解析 port 选项(修复端口参数处理);依赖升级:ws ~8.17.1~8.18.3debug ~4.3.1~4.4.1
  • 6.6.5(2026-05-20):ws 升至 ~8.20.1,修复 CVE-2026-45736。ws 维护者说明:该漏洞 CVSS 评级为 medium,但实际严重性偏低,仅在实际不太可能出现的误用场景下可被利用。
  • 6.6.6(2026-06-16,当前版本):ws 升至 ~8.21.0(修复 CVE-2026-48779);修复 "preserve transport literal suggestions"——保留 transports 选项中的字符串字面量建议,避免类型收窄后丢失用户显式传入的传输顺序信息。

可以看到 6.6.x 的补丁版本以"依赖安全升级 + 边界修复"为主,Bundle 体积稳定在 8.7~8.9 KB。

构建产物、运行与测试方式

构建与测试脚本

package.json 中定义了完整脚本集:

"scripts": {
  "compile": "rimraf ./build && tsc && tsc -p tsconfig.esm.json && ./postcompile.sh",
  "build": "rimraf ./dist && rollup -c support/rollup.config.umd.js && rollup -c support/rollup.config.esm.js",
  "bundle-size": "node support/bundle-size.js",
  "test": "npm run format:check && npm run compile && if test \"$BROWSERS\" = \"1\" ; then npm run test:browser; else npm run test:node; fi",
  "test:node": "mocha --bail --require test/support/hooks.js test/index.js test/webtransport.mjs",
  "test:node-fetch": "USE_FETCH=1 npm run test:node",
  "test:node-builtin-ws": "USE_BUILTIN_WS=1 npm run test:node",
  "test:browser": "zuul test/index.js"
}

其中 test:node-fetchUSE_FETCH=1)与 test:node-builtin-wsUSE_BUILTIN_WS=1)正是对 CHANGELOG 中 "Fetch 需 Node 18+ / 内置 WebSocket 需 Node 21+" 两条脚注的测试侧验证;test:browser 用 zuul 跑浏览器矩阵,Node 侧测试服务器见 test/support/server.js,WebTransport 端到端测试见 test/webtransport.mjs

产物形态

  • build/ 下三套模块:cjsesm(无 debug)、esm-debug(含 debug);
  • dist/ 下三个 Bundle:engine.io.js(UMD 未压缩)、engine.io.min.js(UMD 压缩)、engine.io.esm.min.js(ESM 压缩);
  • 浏览器端还可通过 lib/browser-entrypoint.ts 看到默认的工厂函数形态 default (uri, opts) => new Socket(uri, opts)

如需了解客户端在 socket.io-client 中的封装方式,可继续阅读 packages/socket.io-client/lib/index.tspackages/socket.io-client/lib/manager.ts;服务端行为对照 packages/engine.io/lib/server.ts

版本选择与兼容性小结

基于 CHANGELOG 的事实边界,给出选型要点:

  1. 新项目直接用 6.6.x 主分支(当前 6.6.6)。6.x 客户端与服务端在 Engine.IO 协议 v4 下互通,v5 客户端亦可达 v6 服务端(6.0.0 发布说明明确协议未变)。
  2. 仍依赖 3.x 老服务端的场景使用 3.5.x 维护分支(最新 3.5.4,仅含安全依赖升级);注意 4.0.0 起 ping-pong 机制反转,3.x 客户端无法连接 v4+ 服务端。
  3. 需要控制 Bundle 体积或裁剪传输层时,优先采用 6.6.0+ 的 SocketWithoutUpgrade/SocketWithUpgrade 显式构造 + transports 传类数组方案。
  4. 遇到"轮询失败但 WebSocket 其实可用"的弱网/代理场景,评估开启 tryAllTransports: true 的代价(失败探测耗时增加)。
  5. 依赖版本以 package.json 为准:ws 当前为 ~8.21.0engine.io-parser~5.2.1;安全相关升级(CVE-2026-45736、CVE-2026-48779)都已包含在 6.6.5/6.6.6 中。

整体来看,engine.io-client 的演进轨迹是清晰的:4.0.0 定下新的心跳契约,5.x 补齐 Node.js 长驻进程与页面生命周期场景,6.0.0 完成 TypeScript 与构建现代化,6.5/6.6 再把传输层做成可插拔、可裁剪、可兜底的模块。CHANGELOG 中的每一条记录,如今都能在 packages/engine.io-client/lib/ 的源码中找到对应实现。

登录后查看全文
热门项目推荐
相关项目推荐