Skip to page content

useWebSocket 组合式 API
v2.34+

useWebSocket() composable 让你在组件里维持一条 WebSocket 连接:它会打开这个 socket,把最新收到的消息以响应式值的形式暴露出来,在 socket 尚未就绪时把你要发送的内容排队,连接断开时按退避(backoff)策略自动重连(浏览器恢复在线时会立即重连),还能发送心跳,并在组件销毁时关闭 socket。

TIP

在 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>
内容安全策略(Content Security Policy)

socket 的 URL 必须被你 CSP 的 connect-src 指令所允许。