Vue3项目与桌面端(C++)通过Websocket 对接接口方案实现
文章目录
- 前言
- 业务场景:前端项目与 C++ 后端通过 WebSocket 进行数据通信
- 请求参数示例:
- 返回参数示例:
- 一、WebSocket Hook 封装
- 二、消息分发方法实现
- 三、实现通过 websocket 推送、接收消息
- 3.1 App.vue 中创建 websocket 连接、断开链接
- 3.2 页面推送消息
- 3.2.1 安装 emitter
- 3.2.2 App.vue 文件中暴露方法
- 3.2.3 wsDispatcher.ts 中接收
- 3.2.4 .vue 文件中使用
- 3.3 页面接收消息(通过注册 handler 实现)
- 3.4 页面接收消息Pro版本写法,实现批量注册,自动卸载
- 3.4.1 封装 useWebSocketHandler.ts hooks
- 3.4.2 页面使用
前言
业务场景:前端项目与 C++ 后端通过 WebSocket 进行数据通信
数据格式采用 JSON,通过 apiName 字段区分接口。 需要实现单 WebSocket 连接,所有页面都能通过该连接向 C++ 发送消息,并获取指定接口的返回数据
请求参数示例:
const requestQuery = {apiName: 'accountLogin',data: {username: 'admin',password: '123456'}
}
返回参数示例:
const requestQuery = {apiName: 'accountLoginResponse',data: {code: '200',msg: '登录成功'}
}
一、WebSocket Hook 封装
常规 WebSocket 封装实现,熟悉 WebSocket 的读者可直接查看完整代码:
// /src/hooks/useWebsocket.ts
import { ref, watch, type Ref } from 'vue-demi'
import { type Fn, type MaybeRefOrGetter, isClient, isWorker, toRef, tryOnScopeDispose, useIntervalFn } from '@vueuse/shared'
import { useEventListener } from '@vueuse/core'import { isNullAndUnDef } from '@/utils/is'export type WebSocketStatus = 'OPEN' | 'CONNECTING' | 'CLOSED'const DEFAULT_PING_MESSAGE = 'ping'/**** @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket*/
export interface UseWebSocketOptions {onConnected?: (ws: WebSocket) => voidonDisconnected?: (ws: WebSocket, event: CloseEvent) => voidonError?: (ws: WebSocket, event: Event) => voidonMessage?: (ws: WebSocket, event: MessageEvent) => void/*** Send heartbeat for every x milliseconds passed* 每隔 x ms 发送一次心跳** @default false*/heartbeat?:| boolean| {/*** The message to send* 发送的消息** @default 'ping'*/message?: string | ArrayBuffer | Blob/*** Interval, in milliseconds* 每隔多少毫秒发送一次心跳** @default 1000*/interval?: number/*** Heartbeat response timeout, in milliseconds* 心跳响应超时时间,单位毫秒** @default 1000*/pongTimeout?: number}/*** Enabled auto reconnect* 是否开启自动重连** @default false*/autoReconnect?:| boolean| {/*** Maximum retry times.* Or you can pass predicate function (which returns true if you want to retry).* 最大重试次数* 或者传入一个断言函数(布尔表达式),当函数返回 true 时重连** @default -1*/retries?: number | (() => boolean)/*** Delay for reconnect, in milliseconds* 重连延迟,单位毫秒** @default 1000*/delay?: number/*** On maximum retry times reached* 达到最大重试次数时触发*/onFailed?: Fn}/*** Automatically open a connection* 是否自动打开连接** @default true*/immediate?: boolean/*** Automatically close a connection* 是否自动关闭连接** @default true*/autoClose?: true/*** List of one or more sub-protocol string* 一个或多个子协议字符串** @default []*/protocols?: string[]
}export interface UseWebSocketReturn<T> {/*** 通过 websocket 接收到的最新数据的引用,* 可以监听它以响应传入的消息*/data: Ref<T | null>/*** 当前 websocket 状态可能的值:* 'OPEN', 'CONNECTING', 'CLOSED'*/status: Ref<WebSocketStatus>/*** 关闭 websocket 连接*/close: WebSocket['close']/*** 重新打开 websocket 连接。* 如果当前连接处于活动状态,则在打开新连接之前将其关闭。*/open: Fn/** Sends data through the websocket connection.* 通过 websocket 连接发送数据。** @param data* @param useBuffer 当 socket 连接未打开时,将数据存储到缓冲区并在连接时发送。默认为 true。*/send: (data: string | ArrayBuffer | Blob, useBuffer?: boolean) => boolean/*** WebSocket 实例的引用。*/ws: Ref<WebSocket | undefined>
}/*** 处理嵌套的选项* @param options*/
function resolveNestedOptions<T>(options: T | true): T {if (options === true) {return {} as T}return options
}/*** 响应式的 WebSocket 客户端。* @useage* @see hooks/docs/useWebsocket.md || https://vueuse.org/useWebSocket* @param url* @param options*/
export function useWebSocket<Data = any>(url: MaybeRefOrGetter<string | URL | undefined>,options: UseWebSocketOptions = {}
): UseWebSocketReturn<Data> {const { onConnected, onDisconnected, onError, onMessage, immediate = true, autoClose = true, protocols = [] } = optionsconst data: Ref<Data | null> = ref(null)const status = ref<WebSocketStatus>('CLOSED')const wsRef = ref<WebSocket | undefined>()const urlRef = toRef(url)// 心跳暂停let heartbeatPause: Fn | undefined// 心跳恢复let heartbeatResume: Fn | undefined// 连接是否已完全关闭let explicitlyClosed = false// 重连次数let retried = 0// 缓冲 datalet bufferedData: (string | ArrayBuffer | Blob)[] = []// ping pong timeout 时长let pongTimeoutWait: ReturnType<typeof setTimeout> | undefined/*** 发送缓冲数据*/const _sendBuffer = () => {if (bufferedData.length && wsRef.value && status.value === 'OPEN') {for (const buffer of bufferedData) {wsRef.value.send(buffer)}// 清空缓冲bufferedData = []}}/*** 重置心跳*/const resetHeartbeat = () => {clearTimeout(pongTimeoutWait)pongTimeoutWait = undefined}// Status code 1000 -> Normal closure https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code/*** 正常关闭* @param code* @param reason*/const close: WebSocket['close'] = (code = 1000, reason) => {if (!isClient || !wsRef.value) returnexplicitlyClosed = trueresetHeartbeat()heartbeatPause?.()wsRef.value?.close(code, reason)}/*** 发送数据* @param data* @param useBuffer*/const send = (data: string | ArrayBuffer | Blob, useBuffer = true) => {if (!wsRef.value || status.value !== 'OPEN') {useBuffer && bufferedData.push(data)return false}// 先把断开连接时未发送的缓存数据先发送出去_sendBuffer()// 再发送当前数据wsRef.value.send(data)return true}/*** 初始化 websocket*/const _init = () => {// 正常关闭时不重连if (explicitlyClosed || typeof urlRef.value === 'undefined') returnconst ws = new WebSocket(urlRef.value, protocols)wsRef.value = wsstatus.value = 'CONNECTING'/*** 连接成功*/ws.onopen = () => {status.value = 'OPEN'onConnected?.(ws)heartbeatResume?.()// 将缓存的数据发送出去_sendBuffer()}/*** 连接断开* @param ev*/ws.onclose = ev => {status.value = 'CLOSED'wsRef.value = undefinedonDisconnected?.(ws, ev)// 不是正常关闭时,重连if (!explicitlyClosed && options.autoReconnect) {const { retries = -1, delay = 1000, onFailed } = resolveNestedOptions(options.autoReconnect)retried += 1if (typeof retries === 'number' && (retries < 0 || retried < retries)) {setTimeout(_init, delay)} else if (typeof retries === 'function' && retries()) {setTimeout(_init, delay)} else {onFailed?.()}}}/*** 连接错误* @param e*/ws.onerror = e => {onError?.(ws!, e)}/*** 接收消息* @param e*/ws.onmessage = (e: MessageEvent) => {if (options.heartbeat) {// 重置心跳计时// 以保持心跳的频率和稳定性resetHeartbeat()const { message = DEFAULT_PING_MESSAGE } = resolveNestedOptions(options.heartbeat)// 服务器发送的是心跳消息,return// 避免将心跳消息当作普通消息处理if (e.data === message) {return}}data.value = e.dataonMessage?.(ws!, e)}}// 心跳if (options.heartbeat) {const { message = DEFAULT_PING_MESSAGE, interval = 1000, pongTimeout = 1000 } = resolveNestedOptions(options.heartbeat)const { pause, resume } = useIntervalFn(() => {// 发送心跳send(message, false)if (!isNullAndUnDef(pongTimeoutWait)) return// 设置心跳超时pongTimeoutWait = setTimeout(() => {// 超时后关闭连接// auto-reconnect will be trigger with ws.onclose()close()explicitlyClosed = false}, pongTimeout)},interval,{ immediate: false })heartbeatPause = pauseheartbeatResume = resume}if (autoClose) {if (isClient) {useEventListener('beforeunload', () => close())}tryOnScopeDispose(() => close())}/*** 开启连接*/const open = () => {if (!isClient && !isWorker) returnclose()explicitlyClosed = falseretried = 0_init()}if (immediate) {watch(urlRef, open, { immediate: true })}return {data,status,close,send,open,ws: wsRef}
}
二、消息分发方法实现
// /src/utils/wsDispatcher.ts// 定义处理函数类型
type WsHandler = (data: any) => void
// 存储处理函数的映射
const handlers = new Map<string, WsHandler>()/*** 注册响应函数* @param apiName 接口名称* @param handler 处理函数* @returns 取消注册函数*/
export function registerHandler(apiName: string, handler: WsHandler) {handlers.set(apiName, handler)return () => handlers.delete(apiName)
}/*** 处理WebSocket消息* @param message 消息内容* @returns void*/
export function dispatchMessage(message: string) {try {const { apiName, data } = JSON.parse(message)const handler = handlers.get(apiName)if (handler) {handler(data)} else {console.warn(`未找到处理函数:${apiName}`)}} catch (e) {console.error('WebSocket消息解析失败', e)}
}
三、实现通过 websocket 推送、接收消息
3.1 App.vue 中创建 websocket 连接、断开链接
在 App.vue 中创建、销毁 websocket,通过 dispatchMessage 将消息分发到每个页面
<template><el-config-provider :locale="locale"><router-view /></el-config-provider>
</template><script lang="ts" setup>
import { onBeforeUnmount } from 'vue'
import { useWebSocket } from '@/hooks/web/useWebsocket'
import { dispatchMessage } from '@/utils/wsDispatcher'defineOptions({name: 'App'
})// 初始化WebSocket
const { close, send } = useWebSocket('ws://172.16.16.129:12345/', {onConnected: ws => {console.log('WebSocket connected', ws)},onDisconnected: (ws, event) => {console.log('WebSocket disconnected', event)},onError: (ws, event) => {console.error('WebSocket error', event)},onMessage: (ws, event) => {dispatchMessage(event.data) // 使用分发中心处理消息},// 自动重连配置autoReconnect: {retries: 3,delay: 1000},heartbeat: {message: 'ping',interval: 30000,pongTimeout: 10000}
})/*** 发送请求* @param apiName C++接口名* @param data C++接口参数* @return void*/
const sendMessage = (apiName: string, data: any) => {const requestBody = {apiName,data}send(JSON.stringify(requestBody))
}// 页面卸载时关闭WebSocket连接
onBeforeUnmount(() => {close()
})
</script>
3.2 页面推送消息
- 按照 3.1 所写,已经可以有一个 sendMessage 方法去推送消息,但是这个方法怎么被其他的 .vue 文件所使用呢?
- 首先想到的是 vue3 的
Provide 和 Inject,但是实际写下来每个页面里面冗余代码过多,不采用 - 所以,使用 emitter 帮我实现 类似 vue 的 eventBus 的功能
3.2.1 安装 emitter
npm i emitter
3.2.2 App.vue 文件中暴露方法
<script lang="ts" setup>
// ... App.vue 文件中的其他代码,如 import { useWebSocket } from '@/hooks/web/useWebsocket'
import { emitter } from '@/utils/eventBus'// ... App.vue 文件中的其他代码,如 const { close, send } = useWebSocket('ws://172.16.16.129:12345/', {}/*** 发送请求* @param apiName C++接口名* @param data C++接口参数* @return void*/
const sendMessage = (apiName: string, data: any) => {const requestBody = {apiName,data}send(JSON.stringify(requestBody))
}/*** 发送请求* @param apiName C++接口名* @param data C++接口参数* @return void*/
// @ts-ignore
emitter.on('sendMessage', (payload: any) => {sendMessage(payload.apiName, payload.data)
})
</script>
3.2.3 wsDispatcher.ts 中接收
// /src/utils/wsDispatcher.ts
import { emitter } from '@/utils/eventBus'// ... wsDispatcher.ts 的其他的代码/*** 请求Qt端接口* @param apiName 接口名称* @param data 请求数据* @returns void*/
export function sendMessageToQt<T = any>(apiName: string, data: T = {} as T) {emitter.emit('sendMessage', {apiName,data})
}// ... wsDispatcher.ts 的其他的代码
3.2.4 .vue 文件中使用
至此,就可以在 任意的 .vue 文件中去通过 sendMessageToQt 方法,在 websocket 中推送消息
<script setup lang="ts">
import { onMounted} from 'vue'
import { sendMessageToQt } from '@/utils/wsDispatcher'/*** 获取列表* @description querySingleDeviceUsageRecord-查询单个设备借用及归还记录* @returns void*/
const getList = () => {sendMessageToQt('querySingleDeviceUsageRecord', showQuery.value)
}
onMounted(() => {getList()
})
</script>
3.3 页面接收消息(通过注册 handler 实现)
- 现在我们已经可以在不同的 .vue 文件中,注册时间处理器,然后拿到分发过来的对应接口的返回值
- 但是目前这样,我们当前页如果有 5个请求,我们就需要 注册5个Handler,销毁5个Handler,并不友好
- 所以不推荐这种写法,后续有封装
<script lang="ts" setup>
import { onUnmounted} from 'vue'
import { registerHandler} from '@/utils/wsDispatcher'// 处理接口名:apiName1 对应的返回值
const handleTaskOverviewInfo = (res: any) => {// TODO: 拿到 桌面端(C++) 传递的 res,进行后续处理,例如 vue 页面数据渲染
}// 注册事件处理器
const unregisterApiName1Response = registerHandler('apiName1', handleTaskOverviewInfo)onUnmounted(() => {// 组件卸载时自动取消注册unregisterTaskOverviewInfo()
})
</script>
3.4 页面接收消息Pro版本写法,实现批量注册,自动卸载
3.4.1 封装 useWebSocketHandler.ts hooks
import { onUnmounted } from 'vue'
import { registerHandler } from '@/utils/wsDispatcher'/*** 注册事件处理函数* @param eventName 事件名称* @param handler 事件处理函数*/
export const useWebSocketHandler = () => {// 存储事件名称和处理函数的映射关系const eventNames: Record<string, Function | undefined> = {}/*** 注册事件处理函数* @param arr 事件名称和处理函数的数组* @example* registerHandlers([* { eventName: 'getLoginRecords', handler: handlerGetLoginRecordsResponse },* { eventName: 'accountLoginResponse', handler: handleAccountLoginResponse },* ])*/const registerHandlers = (arr: Array<{ eventName: string; handler: (...args: any[]) => void }>) => {arr.forEach(({ eventName, handler }) => {console.log(`注册--${eventName}`)const handlerName = registerHandler(eventName, handler)eventNames[eventName] = handlerName})}/*** 注销所有事件处理函数*/const unregisterAllHandlers = () => {Object.keys(eventNames).forEach(key => {const handler = eventNames[key]if (typeof handler === 'function') {console.log(`注销--${key}`)handler()}})}onUnmounted(() => {// 组件卸载时注销所有事件处理函数unregisterAllHandlers()})return {registerHandlers}
}
3.4.2 页面使用
<script setup lang="ts">
import { onMounted, ref } from 'vue'import { sendMessageToQt } from '@/utils/wsDispatcher'
import { useWebSocketHandler } from '@/hooks/web/useWebSocketHandler'defineOptions({name: 'AdminMangeDevice'
})/*** 请求服务器数据* @param pageSize 每页显示条数* @returns void*/
function getList() {sendMessageToQt('apiName1', {offset: 1,counts: 10})
}/*** 处理响应数据*/
function handleApiName1Response(res: any) {// 处理响应数据console.log('apiName1 response:', res)
}/*** 处理响应数据*/
function handleApiName2Response(res: any) {// 处理响应数据console.log('apiName2 response:', res)
}const { registerHandlers } = useWebSocketHandler()
/*** 注册消息处理器*/
registerHandlers([{// 设备管理页-获取所有设备的状态eventName: 'apiName1',handler: handleApiName1Response},{// 设备管理页-标记设备不可用/恢复eventName: 'apiName2',handler: handleApiName2Response}
])
onMounted(() => {// 获取列表数据getList()
})
</script>