在普通的网页数据传输中,基本都使用,前端主动去调用接口,获取数据,同步到页面来进行效果展示,当遇到网页端角标数字展示或者推送信息是,则需要与后端建立长链接,及时同步后段的数据。
虽然我问过后端,他那边的数据也是写的定时任务,每隔凌晨的时候同步一次,但是无所谓,还是让我们继续讨论前端如何实现,常规方式无非就是使用轮循去隔几分钟调用接口,但是过于低级,这里我们使用SSE单通道的方式,相当于与后端约定好事件,由后端主动发动数据量,
大概是如下这样的一个场景:
用户接收到3条预警,点击进入预警详情,处理完一条预警,然后后端那边记录操作入库,数据库更改,处理推送,推给前端新的预警数据,则处理完之后,3条就会无缝变成两条。接下来是代码实现方式。
import { fetchEventSource } from '@microsoft/fetch-event-source' import { onUnmounted, ref } from 'vue' interface UseSseOptions<T = any> { url: string /** 需要监听的事件名,后端event字段为空默认等价 message */ event?: string onMessage: (data: T) => void onError?: (error: any) => void autoReconnect?: boolean reconnectInterval?: number maxReconnectAttempts?: number /** 额外自定义请求头 */ headers?: Record<string, string> /** 页面隐藏时是否保持连接 */ openWhenHidden?: boolean } /** * SSE 连接封装(基于 @microsoft/fetch-event-source) * - 自动携带token,支持自定义headers * - 自动重连,最大重连次数限制 * - 组件卸载自动关闭,防止多连接并发 */ export function useSse<T = any>(options: UseSseOptions<T>) { const { url, event = 'message', onMessage, onError, autoReconnect = true, reconnectInterval = 3000, maxReconnectAttempts = 5, headers: customHeaders = {}, openWhenHidden = false, } = options let controller: AbortController | null = null let reconnectCount = 0 let closed = false let connecting = false async function connect() { // 防止并发:已有连接正在建立,直接返回 if (connecting) return close() closed = false connecting = true try { const token = localStorage.getItem('token') const headers: Record<string, string> = { ...customHeaders } if (token) headers.Authorization = `Bearer ${token}` controller = new AbortController() await fetchEventSource(url, { headers, signal: controller.signal, openWhenHidden, onopen: async (response) => { if (response.ok && response.headers.get('content-type')?.includes('text/event-stream')) { reconnectCount = 0 connecting = false } else { throw new Error(`SSE 连接失败: HTTP ${response.status}`) } }, onmessage: (ev) => { try { // 后端event为空字符串,视作 message const incomingEvent = ev.event || 'message' if (incomingEvent !== event) return let payload: T try { payload = JSON.parse(ev.data) console.warn('[SSE] 收到消息', payload) } catch { payload = ev.data as unknown as T } onMessage(payload) } catch (e) { console.error('[SSE] onMessage回调异常', e) } }, onerror: (err) => { connecting = false if (closed) return undefined console.error('[SSE] 连接异常', err) if (!autoReconnect || reconnectCount >= maxReconnectAttempts) { console.warn(`[SSE] 达到最大重连次数${maxReconnectAttempts},停止重连`) // 必须 throw 才能真正停止重连;return undefined 会被库兜底为默认 retryInterval 继续重连 throw err } reconnectCount++ console.warn(`[SSE] 第 ${reconnectCount} 次重连...`) return reconnectInterval }, }) } catch (err) { connecting = false if (!closed) { console.error('[SSE] 连接结束', err) onError?.(err) } } } function close() { closed = true if (controller) { controller.abort() controller = null } } connect() onUnmounted(close) return { close } } /** * 获取通知数量(SSE) * 开发环境自动启用 mock 模式,每 3 秒随机变化一次 */ export function useNotificationCount(sseUrl: string) { const count = ref(0) let closeSse: (() => void) | null = null if (import.meta.env.DEV) { const timer = setInterval(() => { count.value = Math.floor(Math.random() * 150) }, 3000) onUnmounted(() => clearInterval(timer)) } else { const sse = useSse<{ count?: number, num?: number }>({ url: sseUrl, // 后端通过命名事件 init-badge 下发初始数量 event: 'init-badge', onMessage: (data) => { if (data && typeof data !== 'string') { const value = (data as { count?: number, num?: number }).count ?? (data as { count?: number, num?: number }).num if (typeof value !== 'undefined') count.value = Number(value) || 0 } }, }) closeSse = sse.close } return { count, close: closeSse } }那具体我会在公共头部组件中引入,来使用
import { useNotificationCount } from '~/utils/sse' // SSE使用,count是我这边跟后端约定好的,具体可以跟你的后端对接 const { count } = useNotificationCount('/api接口') <template> <div>{{count}}</div> </template>