在 Airi 项目中使用 VueUse useWebSocket 构建生产级响应式 WebSocket 客户端
在 Airi 项目中使用 VueUse useWebSocket 构建生产级响应式 WebSocket 客户端【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi本篇技术指南以仓库内 .agents/skills/vueuse-functions/references/useWebSocket.md 为骨架系统讲解 VueUse 响应式 WebSocket 客户端useWebSocket的完整 API返回值、生命周期回调、immediate/autoConnect/autoClose/autoReconnect/heartbeat/子协议等全部配置项与 TypeScript 类型声明并结合 Airi 仓库内 ws-client.ts 等真实生产代码展示指数退避重连、认证关闭码、消息缓冲、状态机映射等进阶实战方案。读完本文你将能直接复制这些模式到自己的 Vue 3 / Nuxt 3 项目中实现可自动重连、有心跳保活、能优雅处理鉴权失败的 WebSocket 通信层。一、useWebSocket 是什么useWebSocket是 VueUse 的 Network 分类下的响应式 WebSocket 客户端底层封装了浏览器原生 WebSocket API在 Electron 渲染进程/Web/移动端 WebView 场景下均可用把原生的事件驱动模型转换成 Vue 的响应式模型收到消息自动写入dataref模板和watch直接可用连接状态用OPEN | CONNECTING | CLOSED三个字符串表示方便 UI 绑定提供send、open、close命令式控制函数可选自动重连、自动心跳、URL 变化自动切换连接。在 VueUse 的技能决策体系中它属于AUTO级别调用见 SKILL.md 的 Network 分类表格即只要项目里需要 WebSocket 通信就应优先使用它而非手写原生连接管理。它要求 Vue 3或以上/ Nuxt 3或以上项目环境。二、最小用法与返回值2.1 基础调用import { useWebSocket } from vueuse/core const { status, data, send, open, close, ws } useWebSocket(ws://websocketurl)只传入 URL 即可默认immediate开启调用组合式函数后连接立即建立默认autoClose开启组件卸载或 effect scope 停止时自动关闭连接。2.2 返回值速查表属性类型说明dataRefany最近一次收到的数据ShallowRefT \| nullstatusRefOPEN \| CONNECTING \| CLOSED连接状态wsRefWebSocketWebSocket 实例引用ShallowRefWebSocket \| undefinedsend(data, useBuffer?) boolean发送数据未连接时默认缓冲open() void打开/重开连接若已有活动连接会先关闭再重开close(code?, reason?) void关闭连接send的签名细节值得注意来自文档 Type Declarationssend: (data: string | ArrayBuffer | Blob, useBuffer?: boolean) booleandata支持string | ArrayBuffer | Blob三种载荷useBuffer默认true当 socket 尚未打开时数据先存入内部缓冲连接建立后自动补发返回boolean表示本次发送是否直接成功还是进入了缓冲。close直接复用原生WebSocket[close]签名因此可以传入关闭码与原因// 关闭码使用 IANA 保留的应用区间 4000-4999Airi 用它区分鉴权失败与网络抖动 close(4001, invalid authentication response)三、生命周期回调useWebSocket的第二个参数接收选项对象其中包含四个连接生命周期回调const { data } useWebSocket(ws://websocketurl, { onConnected(ws) { console.log(Connected!) }, onDisconnected(ws, event) { console.log(Disconnected!, event.code) }, onError(ws, event) { console.error(Error:, event) }, onMessage(ws, event) { console.log(Message:, event.data) }, })四个回调的完整类型签名来自 Type DeclarationsonConnected?: (ws: WebSocket) void onDisconnected?: (ws: WebSocket, event: CloseEvent) void onError?: (ws: WebSocket, event: Event) void onMessage?: (ws: WebSocket, event: MessageEvent) void关键特性回调拿到的ws是当前连接实例。在启用自动重连后每次重连都会产生一个新的原生 WebSocket 实例因此回调内引用的ws必须来自参数而不是闭包捕获的旧实例——这正是 Airi 在 ws-client.ts 中用activeSocket记录当前连接代数并对回调做代数校验activeSocket ! rawWs则丢弃回调结果的原因。四、连接生命周期选项4.1 immediate默认开启immediate: true默认值表示组合式函数被调用时立即建立连接。关闭后则必须手动调用open()才会连接适用于用户登录后才建立连接的场景。4.2 autoConnect默认开启autoConnect: true默认值的核心能力是当 URL 以 ref/getter 形式传入且值发生变化时自动关闭旧连接并重连到新 URL。这是实现服务器地址切换 / 鉴权令牌轮换触发重连的官方机制。Airi 正是利用这一点把useWebSocket的 URL 参数换成一个ComputedRefstring | undefined见 createChatWsUrlRef 与 ws-client.test.ts 的createChatWsUrlRef用例enabled为false或拿不到 token 时返回undefineduseWebSocket会干净地关闭连接且不触发重连循环token 轮换时 URL ref 重新计算触发 VueUse 的自动重连到同一个地址新 token 在连接建立后再通过应用层协议发送见下文鉴权小节。该函数还有配套的回归测试freezes ws URL when getToken is non-reactive (regression guard)证明getToken必须读取 Vue 响应式源Pinia store、ref、computed否则 URL 永远不会变化。4.3 autoClose默认开启autoClose: true默认值会在触发beforeunload事件或关联的 effect scope 被停止时自动调用close()。这意味着在组件内调用时组件卸载即自动断开无需手写onUnmounted清理在 Pinia setup store 中调用时生命周期与 store 的 effect scope 绑定。4.4 手动 open / close 的语义open()重开连接若当前已有活动连接会先关闭再新建close()优雅关闭显式调用close()不会触发自动重连VueUse 内部会置位explicitlyClosed标志。Airi 利用这一点实现鉴权失败即暂停重连在onDisconnected中收到 4001 关闭码时调用ws.close()让 VueUse 跳过下一次onclose的重连调度源码注释 ws-client.ts 明确记录了该根因。五、autoReconnect 自动重连默认关闭false。开启后连接因错误/异常关闭会自动重连但显式close()不会触发重连。5.1 简单开关const { status, data, close } useWebSocket(ws://websocketurl, { autoReconnect: true, })5.2 精细控制const { status, data, close } useWebSocket(ws://websocketurl, { autoReconnect: { retries: 3, delay: 1000, onFailed() { alert(Failed to connect WebSocket after 3 retries) }, }, })5.3 指数退避delay支持传函数根据当前重试次数计算延迟可实现指数退避const { status, data, close } useWebSocket(ws://websocketurl, { autoReconnect: { retries: 5, // Exponential backoff: 1s, 2s, 4s, 8s, 16s delay: retries Math.min(1000 * 2 ** (retries - 1), 30000), }, })5.4 线性退避const { status, data, close } useWebSocket(ws://websocketurl, { autoReconnect: { retries: 5, // Linear backoff: 1s, 2s, 3s, 4s, 5s delay: retries retries * 1000, }, })5.5 选项类型与默认值来自文档 Type DeclarationsautoReconnect?: | boolean | { /** * Maximum retry times. * Or you can pass a predicate function (which returns true if you want to retry). * default -1 */ retries?: number | ((retried: number) boolean) /** * Delay for reconnect, in milliseconds * Or you can pass a function to calculate the delay based on the number of retries. * default 1000 */ delay?: number | ((retries: number) number) /** * On maximum retry times reached. */ onFailed?: Fn }要点retries默认-1表示无限重试也可以传谓词函数(retried) boolean返回true则继续重试delay默认1000ms可传函数按重试次数动态计算onFailed在达到最大重试次数时触发。5.6 生产级重连Airi 的 bounded jitter 实现Airi 在 ws-client.ts 中把autoReconnect用到了生产级水准const RECONNECT_BASE_MS 1000 const RECONNECT_MAX_MS 30_000 const RECONNECT_RETRIES -1 // 无限重试 const ws useWebSocketstring(urlRef, { immediate: false, autoClose: true, autoReconnect: { retries: RECONNECT_RETRIES, delay: retries computeReconnectDelay( Math.max(retries, authenticationFailures), RECONNECT_BASE_MS, RECONNECT_MAX_MS, ), }, // ...回调见下文 })其配套的延迟计算函数computeReconnectDelay在指数退避之外叠加了带下界的抖动jitterexport function computeReconnectDelay(retries: number, baseMs: number, maxMs: number): number { const exp Math.min(maxMs, baseMs * 2 ** Math.max(0, retries - 1)) // 50% floor 50% jitter window; total range is [exp/2, exp). return Math.floor(exp * 0.5 Math.random() * exp * 0.5) }源码注释解释了这么做的动机VueUse 的autoReconnect从retries 1开始计第一次重连如果直接使用0..exp的均匀随机抖动首次重连可能在一瞬间约 0ms就触发当服务器彻底宕机时多个标签页会同时发起重连风暴reconnect storm。加上 50% 的下界后首次重连落在[500, 1000)区间之后每次翻倍直到 30s 上限。这一行为被 ws-client.test.ts 的computeReconnectDelay测试完整锁定包括retries0的钳制与retries10的封顶。六、heartbeat 心跳保活WebSocket 连接保持空闲时可能被中间代理/负载均衡器掐断常见做法是定时发送小消息保活。useWebSocket内置了心跳辅助器。6.1 简单开启const { status, data, close } useWebSocket(ws://websocketurl, { heartbeat: true, })6.2 精细控制const { status, data, close } useWebSocket(ws://websocketurl, { heartbeat: { message: ping, scheduler: cb useIntervalFn(cb, 2000), pongTimeout: 1000, }, })6.3 完整类型与默认值heartbeat?: | boolean | (ConfigurableScheduler { /** * Message for the heartbeat * default ping */ message?: MaybeRefOrGetterWebSocketHeartbeatMessage /** * Response message for the heartbeat, if undefined the message will be used */ responseMessage?: MaybeRefOrGetterWebSocketHeartbeatMessage /** * Interval, in milliseconds * deprecated Please use scheduler option instead * default 1000 */ interval?: number /** * Heartbeat response timeout, in milliseconds * default 1000 */ pongTimeout?: number })其中WebSocketHeartbeatMessage string | ArrayBuffer | Blob。要点scheduler是更推荐的定时方案interval已标记deprecated它接收一个回调并返回定时器控制句柄典型写法就是组合useIntervalFn(cb, ms)responseMessage若未指定则复用message——即默认以 ping 作为请求与响应消息pongTimeout默认1000ms用于判定对端是否在超时内返回了响应。七、Sub-protocols 子协议原生 WebSocket 构造函数支持第二个参数声明子协议列表useWebSocket通过protocols选项透传const { status, data, send, open, close } useWebSocket(ws://websocketurl, { protocols: [soap], // [soap, wamp] })类型声明为protocols?: string[]默认[]。典型用途是显式协商应用层协议如文档示例中的 SOAP / WAMP服务端会从客户端提供的列表中选择其一并在握手响应中确认。八、完整类型声明以下是文档 Type Declarations 中的完整签名可作为接入时的权威参考export type WebSocketStatus OPEN | CONNECTING | CLOSED export type WebSocketHeartbeatMessage string | ArrayBuffer | Blob export interface UseWebSocketOptions { onConnected?: (ws: WebSocket) void onDisconnected?: (ws: WebSocket, event: CloseEvent) void onError?: (ws: WebSocket, event: Event) void onMessage?: (ws: WebSocket, event: MessageEvent) void /** default false */ heartbeat?: boolean | (ConfigurableScheduler { message?: MaybeRefOrGetterWebSocketHeartbeatMessage // default ping responseMessage?: MaybeRefOrGetterWebSocketHeartbeatMessage interval?: number // deprecated, default 1000 pongTimeout?: number // default 1000 }) /** default false */ autoReconnect?: boolean | { retries?: number | ((retried: number) boolean) // default -1 delay?: number | ((retries: number) number) // default 1000 onFailed?: Fn } /** default true */ immediate?: boolean /** default true */ autoConnect?: boolean /** default true */ autoClose?: boolean /** default [] */ protocols?: string[] } export interface UseWebSocketReturnT { data: ShallowRefT | null status: ShallowRefWebSocketStatus close: WebSocket[close] open: Fn send: (data: string | ArrayBuffer | Blob, useBuffer?: boolean) boolean ws: ShallowRefWebSocket | undefined } export declare function useWebSocketData any( url: MaybeRefOrGetterstring | URL | undefined, options?: UseWebSocketOptions, ): UseWebSocketReturnData几点值得注意url的类型是MaybeRefOrGetterstring | URL | undefined——既可以是普通字符串/URL 对象也可以是 ref 或 getter这正是autoConnect能响应 URL 变化的前提泛型Data决定data的类型Airi 在 ws-client.ts 中写作useWebSocketstring(urlRef, ...)返回值全部是ShallowRef性能上对深层对象只做浅层响应式代理。九、进阶实战Airi 的完整 WebSocket 通信层拆解以下将原文档的 API 知识映射到 Airi 仓库的真实代码展示响应式 WebSocket 认证 重连 状态机的完整拼图。相关文件ws-client.ts、ws-client.test.ts、session-store.ts。9.1 用 URL ref 驱动用户意图与令牌状态Airi 通过createChatWsUrlRef把用户是否想连接 是否有 token编码进 URL refexport function createChatWsUrlRef( enabled: Refboolean, getToken: () string | null, serverUrl: string, ): ComputedRefstring | undefined { return computed(() { if (!enabled.value) return undefined const token getToken() if (!token) return undefined return buildChatWsUrl(serverUrl) }) }配套的buildChatWsUrl使用 URL 解析而非字符串拼接干净地处理协议升级https→wss、http→ws、尾斜杠归一化和查询参数清除防止旧 token 泄漏进 URLexport function buildChatWsUrl(serverUrl: string): string { const url new URL(serverUrl) url.protocol url.protocol https: ? wss: : ws: url.pathname ${url.pathname.replace(/\/$/, )}/ws/v2/chat url.search return url.toString() }测试用例确认https://api.example.com→wss://api.example.com/ws/v2/chat且?token...会被剥离。9.2 连接后鉴权回调 关闭码协议由于浏览器在 WebSocket 升级被拒绝时吞掉 HTTP 401 状态客户端只能看到code1006异常关闭无法区分令牌错误与网络抖动。Airi 的协议方案是服务端先接受升级待请求鉴权失败后以自定义应用关闭码 4001 关闭IANA 私有区间 4000-4999export const WS_CLOSE_UNAUTHORIZED 4001客户端在onConnected里立刻发送鉴权 RPC此处通过 eventa 的defineInvoke调用chat:authenticate成功才置位authenticated失败则用原生rawWs.close(1011, invalid authentication response)关闭当前 socket注意注释强调关闭原生实例而非 VueUse 包装层因为包装层关闭会标记为显式断开、禁用重试调度。在onDisconnected中若关闭码是 4001则调用ws.close()暂停重连直到 token 轮换if (restartForNewToken enabled.value tokenRef.value) { ws.open() } else if (ev.code WS_CLOSE_UNAUTHORIZED) { console.warn([chat-ws] server rejected auth (4001), pausing reconnect until token rotates) ws.close() }对应的测试matches the server-side close code contract (4001, IANA private range)把该协议常量锁定为 4001注释警告服务端常量必须保持一致否则关闭码契约会静默破裂。9.3 三态到四态的状态机映射VueUse 只暴露OPEN | CONNECTING | CLOSED三态而聊天同步层需要区分从未连接 / 显式断开idle与丢了连接、自动重连进行中closed。Airi 用mapStatus做映射并用enabled记录用户意图export function mapStatus(vue: OPEN | CONNECTING | CLOSED, enabled: boolean, authenticated true): ChatWsStatus { if (vue OPEN) return authenticated ? open : connecting if (vue CONNECTING) return connecting return enabled ? closed : idle }其四态为idle | connecting | open | closed。测试还锁定了一个关键根因VueUse 在鉴权 invoke 完成前就会把传输层状态置为OPEN若此时直接对外发布open聊天同步层会在尚未拿到鉴权上下文时调用 RPC。因此mapStatus(OPEN, true, false)必须返回connecting见 ws-client.test.ts。9.4 消息缓冲与监听器隔离Airi 在 store 层还实现了pendingSend 缓冲连接未就绪时把事件推入pendingSendonReady后flush()以及监听器隔离单个 handler 抛错不影响其他订阅者这些与useWebSocket的send(data, useBuffer)缓冲语义互为补充见 channel-server.ts。此外websocket-inspector.ts 与 websocket-inspector.vue 构成一个最多保留 1000 条、区分 incoming/outgoing 与心跳事件的可视化调试面板可作为排查 WebSocket 通信问题的辅助工具。9.5 集成进 Pinia store 的调用形态session-store.ts 展示了在 Pinia setup store 中如何使用上述客户端wsClient createChatWsClient({ serverUrl: SERVER_URL, getToken: () authToken.value, // 响应式读取见 createChatWsUrlRef 契约 }) wsClient.onNewMessages((payload) { /* 合并云端消息按消息 id 去重 */ }) wsClient.onStatusChange((status) { if (status open) void reconcileCloudSessions() // 每次重连成功都做一次补偿拉取 }) wsClient.connect()注释点明VueUseuseWebSocket的connect是同步的只是翻转 URL 驱动的 autoConnect失败不会以 rejected promise 形式暴露而是通过状态监听与自动重连循环体现——这正是响应式模型与原生 Promise 模型的根本差异。十、总结与选型建议useWebSocket是一套开箱即用的响应式 WebSocket 封装默认的immediate/autoConnect/autoClose让最简用法只有一行代码autoReconnect提供可编程重试次数、延迟函数与失败回调足以支撑指数/线性退避乃至带 jitter 的生产级策略heartbeat一条配置即可实现保活protocols支持子协议协商URL 支持 ref/getter 又为服务器切换、令牌轮换自动重连这类动态场景提供了官方通道。结合 Airi 的实战可以总结出四条可复用的工程模式用 URL ref 统一承载用户意图 令牌可用性undefined即断开且不进入重连循环用自定义关闭码4000-4999 区间承载应用层错误语义弥补浏览器吞掉 HTTP 401 的信息缺口并在收到致命关闭码时调用close()暂停 VueUse 的自动重连给指数退避叠加 50% 下界抖动避免多标签页在服务端宕机时形成重连风暴在三态status之上再叠加业务状态如鉴权是否完成映射成自己的状态机防止连接已打开但业务尚未就绪时提前触发 RPC。若需进一步查阅可前往 .agents/skills/vueuse-functions/references/useWebSocket.md 查看原始文档与类型声明或阅读 packages/stage-ui/src/libs/chat-sync/ws-client.ts 及其测试 ws-client.test.ts 获取可运行的完整实现。【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考