Skip to page content

useEventSource 组合式 API
v2.34+

useEventSource() composable 让你在组件里接收 Server-Sent Events(服务器推送事件):它会打开一条 EventSource 流,把最新收到的事件以响应式值的形式暴露出来,监听你所指定的命名事件,在浏览器已放弃的流上重新打开连接(按退避策略,浏览器恢复在线时会立即重连),并在组件销毁时关闭该流。

Server-Sent Events 是一条单向通道:服务器通过一个普通的 HTTP 响应推送文本事件,而浏览器在连接断开时会自行重连。如果你需要双向连接,请参阅 useWebSocket。

TIP

在 SSR 或 SSG 模式的服务端,不会创建任何流:sourceStatus 始终保持 closed,也不会有任何事件到达。流会在组件挂载到客户端后才打开,因此在 hydration 之前状态同样是 closed。

在组件之外使用

该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。那里没有挂载过程需要等待,因此流会立即打开(除非设置了 lazy),并且不会自行关闭:完成后请调用 closeSource()。它会释放该 composable 持有的一切(流、online/offline 监听器,以及对响应式 url 的 watcher),之后再调用 openSource() 又会把这一切重新建立起来。

语法

import { useEventSource } from 'quasar'

setup () {
  const {
    sourceStatus, sourceData, sourceLastEventId, sourceError, openSource, closeSource
  } = useEventSource(
    url, // String, URL, or a ref/getter of one
    {
      // all optional:

      lazy: true, // do not open the stream on mount;
                  // openSource() does it

      withCredentials: true, // send cookies on a cross-origin URL
      events: ['update'],    // named events to listen to, on top of
                             // the unnamed ("message") ones

      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
      },

      onOpen (evt) { // called each time the stream opens
        // ...
      },
      onMessage (data, evt) { // called with each event received
        // evt.type is the event name, evt.lastEventId its id
      },
      onClose (reason) { // called when the stream closes;
        // reason: 'programmatic' | 'unmount' | 'url' | 'remote'
      },
      onError (evt) { // called with the stream's "error" event
        // ...
      },
      onReconnect (attempt, delay) { // called when a reconnect gets scheduled
        // ...
      }
    }
  )

  // ...
}
function useEventSource(
  url: MaybeRefOrGetter<string | URL>,
  options?: {
    lazy?: boolean
    withCredentials?: boolean
    events?: string[]
    autoReconnect?:
      | boolean
      | {
          retries?: number
          delay?: number | ((attempt: number) => number)
        }
    onOpen?: (evt: Event) => void
    onMessage?: (data: string, evt: MessageEvent<string>) => void
    onClose?: (reason: 'programmatic' | 'unmount' | 'url' | 'remote') => void
    onError?: (evt: Event) => void
    onReconnect?: (attempt: number, delay: number) => void
  }
): {
  sourceStatus: Ref<'closed' | 'connecting' | 'open'>
  sourceData: ShallowRef<string | null>
  sourceLastEventId: ShallowRef<string | null>
  sourceError: ShallowRef<Event | null>
  openSource: () => void
  closeSource: () => void
}

生命周期

流会在组件挂载时打开(如果该 composable 在组件之外使用,则会立即打开),并在组件销毁时关闭。若想让流一直等待你调用 openSource() 为止,请设置 lazy: true。

每一次调用 useEventSource() 只管理一条来自单个端点的流;如果需要多条流,就多次调用它。

closeSource() 会关闭流并停止一切重连。这并不是终结:之后调用 openSource() 会打开一条全新的流。

一旦组件被销毁,该 composable 便完成了使命:openSource() 不再起作用,因此某个迟到的异步回调无法再打开一条没人负责关闭的流。

sourceStatus 从流被请求那一刻起到它打开为止都是 connecting,事件正常流动时为 open,而在从未打开、被你关闭、或重连被放弃时则为 closed。在浏览器自行重试期间,或该 composable 等待重连期间,状态同样是 connecting。

当 url 是一个 ref 或 getter,且在流仍被需要时其值发生变化,当前流会被关闭,并向新的 URL 打开一条新流(比如 query string 里的 token 变了,或换了一个频道)。

接收事件

sourceData 保存最新收到事件的 data(它始终是一个 String:如果你的服务器发送的是 JSON,请自己用 JSON.parse() 解析),而 sourceLastEventId 保存最近一个带有 id 的事件的那个 id。sourceError 保存流最近一次的 error 事件。onMessage、onError、onOpen 和 onClose 这些钩子会在相同的场景下被调用,因此你无需自己去 watch 这些 ref。

默认情况下,EventSource 只会投递那些不带 event: 字段的事件(也就是 “message” 事件)。把你服务器发送的其他事件名列在 events 下,就能一并接收它们;onMessage(data, evt) 会把它们全部收下,其中 evt.type 说明了事件名。

onClose(reason) 会在每一次关闭时被调用,携带一个参数说明是谁发起了关闭:programmatic 表示你调用了 closeSource(),unmount 表示组件被销毁,url 表示流被切换到了新的 URL,remote 表示浏览器放弃了这条连接。对于 remote,该钩子会在任何重连被安排之前运行,因此在其中调用 closeSource() 可以让流保持关闭。

重连

当一条流的连接断开时,浏览器会自行重连(并尊重服务器发送的 retry: 字段),并在每次尝试前触发一个 error 事件;该 composable 会通过 sourceError 和 onError 把它报告出来,此时 sourceStatus 显示为 connecting,除此之外无需你做任何事。

不过,当服务器以 200 之外的状态码作答、以 text/event-stream 之外的内容类型作答,或者连接被直接拒绝(比如服务器重启期间某个代理返回 502)时,浏览器就会放弃。这正是该 composable 介入的时机:这样的流会在延迟一段时间后被重新打开,先是 1 秒,然后翻倍直至 30 秒,会持续尝试直到成功。用 autoReconnect: { retries, delay } 来调整它,其中 delay 是毫秒数,或者是一个以尝试序号(从 0 开始)为参数的函数;也可以用 autoReconnect: false 关闭它。一次成功连接会重置尝试计数。每当安排一次重连时都会调用 onReconnect(attempt, delay),携带尝试次数(从 1 开始)和需要等待的毫秒数;当重试次数用尽时,sourceStatus 变为 closed。

浏览器报告处于离线状态时不会进行任何尝试:该 composable 会等待 online 事件,并在它触发时立即重连(onReconnect(1, 0)),开始新一轮尝试,即便此前重试次数已经用尽也是如此。closeSource() 会终结这一切:流会一直保持关闭,直到你再次调用 openSource()。

由该 composable 重新打开的流(包括它自己的自动重连)是一个全新的 EventSource,因此浏览器不会像在它自行重试时那样发送 Last-Event-ID 请求头。如果你的服务器能够从某个 id 断点续传,请在关闭之后、调用 openSource() 之前,自己把 sourceLastEventId.value 放进 URL 里;要在那里读取它,而不要从 url getter 里读,因为一个读取 sourceLastEventId 的 getter 会在每个带 id 的事件到来时把流切换到新的 URL。

示例

下面这个示例监听 Wikimedia 公开的最近编辑(recent edits)事件流,它每秒会推送好几个事件。这条流会等待你的点击(lazy);打开并关闭它,看看状态如何随之变化。

<template>
  <div class="q-pa-md">
    <div class="row items-center q-gutter-sm q-mb-md">
      <q-btn
        v-if="sourceStatus === 'closed'"
        color="positive"
        label="openSource()"
        no-caps
        @click="openSource"
      />
      <q-btn
        v-else
        color="negative"
        label="closeSource()"
        no-caps
        @click="closeSource"
      />

      <div>
        Status:
        <q-badge
          :color="
            sourceStatus === 'open'
              ? 'positive'
              : sourceStatus === 'connecting'
                ? 'warning'
                : 'grey'
          "
          :label="sourceStatus"
        />
      </div>
    </div>

    <div v-if="log.length === 0">No event received yet</div>
    <div v-for="entry in log" :key="entry.id" class="text-caption ellipsis">
      <strong>{{ entry.wiki }}</strong> {{ entry.title }}
      <span class="text-grey">by {{ entry.user }}</span>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { useEventSource } from 'quasar'

const log = ref([])

// a public stream of the edits made on all Wikimedia projects
const { sourceStatus, openSource, closeSource } = useEventSource(
  'https://stream.wikimedia.org/v2/stream/recentchange',
  {
    lazy: true,
    onMessage(data) {
      const change = JSON.parse(data)
      log.value.unshift({
        id: change.meta.id,
        wiki: change.wiki,
        title: change.title,
        user: change.user
      })
      log.value.length = Math.min(log.value.length, 10)
    }
  }
)
</script>
内容安全策略(Content Security Policy)

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