useWebSocket() composable 让你在组件里维持一条 WebSocket 连接:它会打开这个 socket,把最新收到的消息以响应式值的形式暴露出来,在 socket 尚未就绪时把你要发送的内容排队,连接断开时按退避(backoff)策略自动重连(浏览器恢复在线时会立即重连),还能发送心跳,并在组件销毁时关闭 socket。
在 SSR 或 SSG 模式的服务端,不会创建任何 socket:socketStatus 始终保持 closed,sendSocketMessage() 什么也不做,也不会有任何消息到达。socket 会在组件挂载到客户端后才打开,因此在 hydration 之前状态同样是 closed。
该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。那里没有挂载过程需要等待,因此 socket 会立即打开(除非设置了 lazy),并且不会自行关闭:完成后请调用 closeSocket()。它会释放该 composable 持有的一切(socket、排队的消息、online/offline 监听器,以及对响应式 url 的 watcher),之后再调用 openSocket() 又会把这一切重新建立起来。
语法
import { useWebSocket } from 'quasar'
setup () {
const {
socketStatus, socketData, socketError, sendSocketMessage, openSocket, closeSocket
} = useWebSocket(
url, // String, URL, or a ref/getter of one;
// '/live' and 'https://...' forms are mapped to ws(s)
{
// all optional:
lazy: true, // do not open the socket on mount;
// openSocket() or the first sendSocketMessage() does it
protocols: ['chat'], // the native sub-protocol(s)
binaryType: 'arraybuffer', // 'blob' (default) or 'arraybuffer'
autoReconnect: { // default: true (Infinity retries, 1s doubling up to 30s);
retries: 5, // false disables it
delay: attempt => 500 * (attempt + 1) // ms; a number works too
},
heartbeat: { // default: false; true for the defaults below
message: 'ping', // what to send
interval: 30000 // every X ms while the socket is open
},
onOpen (evt) { // called each time the socket opens
// ...
},
onMessage (data, evt) { // called with each message received
// ...
},
onClose (evt, reason) { // called when the socket closes;
// reason: 'programmatic' | 'unmount' | 'url' | 'remote'
},
onError (evt) { // called with the socket's "error" event
// ...
},
onReconnect (attempt, delay) { // called when a reconnect gets scheduled
// ...
}
}
)
// ...
}function useWebSocket<Data = any>(
url: MaybeRefOrGetter<string | URL>,
options?: {
lazy?: boolean
protocols?: string | string[]
binaryType?: 'blob' | 'arraybuffer'
autoReconnect?:
| boolean
| {
retries?: number
delay?: number | ((attempt: number) => number)
}
heartbeat?:
| boolean
| {
message?: string | ArrayBufferLike | Blob | ArrayBufferView
interval?: number
}
onOpen?: (evt: Event) => void
onMessage?: (data: Data, evt: MessageEvent<Data>) => void
onClose?: (
evt: CloseEvent,
reason: 'programmatic' | 'unmount' | 'url' | 'remote'
) => void
onError?: (evt: Event) => void
onReconnect?: (attempt: number, delay: number) => void
}
): {
socketStatus: Ref<'closed' | 'connecting' | 'open'>
socketData: ShallowRef<Data | null>
socketError: ShallowRef<Event | null>
sendSocketMessage: (
message: string | ArrayBufferLike | Blob | ArrayBufferView
) => void
openSocket: () => void
closeSocket: (code?: number, reason?: string) => void
}生命周期
socket 会在组件挂载时打开(如果该 composable 在组件之外使用,则会立即打开),并在组件销毁时关闭。若想让 socket 一直保持沉默、直到你调用 openSocket()(或第一次调用 sendSocketMessage(),它会自行打开 socket)为止,请设置 lazy: true。
每一次调用 useWebSocket() 只管理一条到单个端点的连接;如果需要多条 socket,就多次调用它。
closeSocket(code, reason) 会用原生的关闭码(close code)和原因关闭连接,丢弃排队中的消息,并停止一切重连。原生约束依然适用:关闭码必须是 1000 或落在 3000 到 4999 区间内,原因最多为 123 字节的 UTF-8;传入无效的组合时,会改用默认值关闭。这并不是终结:之后调用 openSocket() 或 sendSocketMessage() 会打开一条全新的连接。
一旦组件被销毁,该 composable 便完成了使命:openSocket() 和 sendSocketMessage() 都不再起作用,因此某个迟到的异步回调无法再打开一条没人负责关闭的 socket。
socketStatus 从 socket 被请求那一刻起到它打开为止都是 connecting,消息正常流动时为 open,而在从未打开、被你关闭、或重连被放弃时则为 closed。等待重连期间状态同样是 connecting,因为此时该 composable 仍在为它努力。
当 url 是一个 ref 或 getter,且在 socket 仍被需要时其值发生变化,当前连接会被关闭,并向新的 URL 打开一条新连接(比如 query string 里的 token 变了,或换了一个房间)。
url 不一定要是 ws:// 或 wss:// 形式:相对 URL('/api/live')会相对页面进行解析,而 http:// 或 https:// 形式会被映射为 ws:// 或 wss://。以这种方式跟随页面自身协议的 socket,在 https 页面上永远不会因为混合内容(mixed content)被拦截。最新的浏览器原生就接受这类 URL;对于 Quasar 支持的那些较老浏览器,该 composable 也会替它们完成转换。
发送与接收
sendSocketMessage(message) 发送一个 String、Blob、ArrayBuffer 或类型化数组(typed array)。在 socket 尚未打开时发送的消息会被排队,并在它打开的一瞬间按顺序发出;那些在 onOpen 内发送的消息会排在最前面,这样握手(认证、订阅)就能在排队的消息之前抵达服务器。在一条已关闭的 socket 上调用 sendSocketMessage() 会把它打开。closeSocket() 会丢弃仍在排队中的所有内容。
socketData 保存最新收到消息的 data,socketError 保存 socket 最近一次的 error 事件。onMessage、onError、onOpen 和 onClose 这些钩子会在相同的场景下被调用,因此你无需自己去 watch 这些 ref。消息按其发送时的原样抵达:如果你的协议是 JSON,请在 onMessage 里自己解析(JSON.parse())。
onClose(evt, reason) 会在每一次关闭时被调用,携带原生的 CloseEvent(它的 code、reason 和 wasClean 说明了连接是如何结束的),以及第二个参数,用来说明是谁发起了关闭:programmatic 表示你调用了 closeSocket(),unmount 表示组件被销毁,url 表示 socket 被切换到了新的 URL,remote 表示 socket 自行关闭(服务器关闭了它,或者连接断开了)。该钩子在关闭事件到达时运行,因此对于 unmount 而言,此时组件其实已经消失了。对于 remote,它会在任何重连被安排之前运行,因此在其中调用 closeSocket() 可以让 socket 保持关闭。
重连
一条自行关闭的 socket(服务器离线、网络中断)会在延迟一段时间后被重新打开:先是 1 秒,然后翻倍直至 30 秒,会持续尝试直到成功。用 autoReconnect: { retries, delay } 来调整它,其中 delay 是毫秒数,或者是一个以尝试序号(从 0 开始)为参数的函数;也可以用 autoReconnect: false 关闭它。一次成功连接会重置尝试计数。每当安排一次重连时都会调用 onReconnect(attempt, delay),携带尝试次数(从 1 开始)和需要等待的毫秒数,这样「正在重连…」的提示就能说明还要等多久;当重试次数用尽时,socketStatus 变为 closed。
浏览器报告处于离线状态时不会进行任何尝试:该 composable 会等待 online 事件,并在它触发时立即重连(onReconnect(1, 0)),开始新一轮尝试,即便此前重试次数已经用尽也是如此。closeSocket() 会终结这一切:socket 会一直保持关闭,直到你再次调用 openSocket()。
心跳
有些代理和负载均衡器会掐断长时间保持沉默的连接。heartbeat: true 会在 socket 打开期间每 30 秒发送一次字符串 'ping';设置 { message, interval } 来匹配你服务器所期望的内容。心跳会随 socket 一同停止,并在每一次(重新)连接时重新开始。
示例
下面这个示例与一个公共的回显(echo)服务器通信,它会问候每一条新连接,然后把收到的所有内容原样返回。这个 socket 会等待你的点击(lazy);打开它、关闭它,或者把网络断开再开,看看状态如何随之变化。
<template>
<div class="q-pa-md">
<div class="row items-center q-gutter-sm q-mb-md">
<q-input
v-model="message"
dense
outlined
label="Message"
style="width: 220px"
@keyup.enter="sendMessage"
/>
<q-btn
color="primary"
label="sendSocketMessage()"
no-caps
:disable="message === ''"
@click="sendMessage"
/>
<q-btn
v-if="socketStatus === 'closed'"
color="positive"
label="openSocket()"
no-caps
@click="openSocket"
/>
<q-btn
v-else
color="negative"
label="closeSocket()"
no-caps
@click="closeSocket()"
/>
</div>
<div class="q-mb-sm">
Status:
<q-badge
:color="
socketStatus === 'open'
? 'positive'
: socketStatus === 'connecting'
? 'warning'
: 'grey'
"
:label="socketStatus"
/>
</div>
<div v-if="log.length === 0">No message received yet</div>
<div v-for="(entry, index) in log" :key="index" class="text-caption">{{
entry
}}</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useWebSocket } from 'quasar'
const message = ref('Hello Quasar')
const log = ref([])
// a public echo server: it greets each connection, then repeats
// every message it receives
const { socketStatus, sendSocketMessage, openSocket, closeSocket } =
useWebSocket('wss://echo.websocket.org', {
lazy: true,
onMessage(data) {
log.value.unshift(`received: ${data}`)
}
})
function sendMessage() {
log.value.unshift(`sent: ${message.value}`)
sendSocketMessage(message.value)
}
</script>socket 的 URL 必须被你 CSP 的 connect-src 指令所允许。