Vue3项目与桌面端(C++)通过Websocket 对接接口方案实现

文章目录

    • 前言
    • [一、WebSocket Hook 封装](#一、WebSocket Hook 封装)
    • 二、消息分发方法实现
    • [三、实现通过 websocket 推送、接收消息](#三、实现通过 websocket 推送、接收消息)
        • [3.1 App.vue 中创建 websocket 连接、断开链接](#3.1 App.vue 中创建 websocket 连接、断开链接)
        • [3.2 页面推送消息](#3.2 页面推送消息)
            • [3.2.1 安装 emitter](#3.2.1 安装 emitter)
            • [3.2.2 App.vue 文件中暴露方法](#3.2.2 App.vue 文件中暴露方法)
            • [3.2.3 wsDispatcher.ts 中接收](#3.2.3 wsDispatcher.ts 中接收)
            • [3.2.4 .vue 文件中使用](#3.2.4 .vue 文件中使用)
        • [3.3 页面接收消息(通过注册 handler 实现)](#3.3 页面接收消息(通过注册 handler 实现))
        • [3.4 页面接收消息Pro版本写法,实现批量注册,自动卸载](#3.4 页面接收消息Pro版本写法,实现批量注册,自动卸载)
            • [3.4.1 封装 useWebSocketHandler.ts hooks](#3.4.1 封装 useWebSocketHandler.ts hooks)
            • [3.4.2 页面使用](#3.4.2 页面使用)

前言

业务场景:前端项目与 C++ 后端通过 WebSocket 进行数据通信

数据格式采用 JSON,通过 apiName 字段区分接口。 需要实现单 WebSocket 连接,所有页面都能通过该连接向 C++ 发送消息,并获取指定接口的返回数据

请求参数示例:
javascript 复制代码
const requestQuery = {
  apiName: 'accountLogin',
  data: {
    username: 'admin',
    password: '123456'
  }
}
返回参数示例:
javascript 复制代码
const requestQuery = {
  apiName: 'accountLoginResponse',
  data: {
    code: '200',
    msg: '登录成功'
  }
}

一、WebSocket Hook 封装

常规 WebSocket 封装实现,熟悉 WebSocket 的读者可直接查看完整代码:

js 复制代码
// /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) => void
  onDisconnected?: (ws: WebSocket, event: CloseEvent) => void
  onError?: (ws: WebSocket, event: Event) => void
  onMessage?: (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 = [] } = options

  const 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

  // 缓冲 data
  let 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) return
    explicitlyClosed = true
    resetHeartbeat()
    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') return

    const ws = new WebSocket(urlRef.value, protocols)
    wsRef.value = ws
    status.value = 'CONNECTING'

    /**
     * 连接成功
     */
    ws.onopen = () => {
      status.value = 'OPEN'
      onConnected?.(ws)
      heartbeatResume?.()
      // 将缓存的数据发送出去
      _sendBuffer()
    }

    /**
     * 连接断开
     * @param ev
     */
    ws.onclose = ev => {
      status.value = 'CLOSED'
      wsRef.value = undefined
      onDisconnected?.(ws, ev)

      // 不是正常关闭时,重连
      if (!explicitlyClosed && options.autoReconnect) {
        const { retries = -1, delay = 1000, onFailed } = resolveNestedOptions(options.autoReconnect)
        retried += 1

        if (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.data
      onMessage?.(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 = pause
    heartbeatResume = resume
  }

  if (autoClose) {
    if (isClient) {
      useEventListener('beforeunload', () => close())
    }
    tryOnScopeDispose(() => close())
  }

  /**
   * 开启连接
   */
  const open = () => {
    if (!isClient && !isWorker) return

    close()
    explicitlyClosed = false
    retried = 0
    _init()
  }

  if (immediate) {
    watch(urlRef, open, { immediate: true })
  }

  return {
    data,
    status,
    close,
    send,
    open,
    ws: wsRef
  }
}

二、消息分发方法实现

javascript 复制代码
// /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 将消息分发到每个页面

javascript 复制代码
<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
bash 复制代码
npm i emitter
3.2.2 App.vue 文件中暴露方法
javascript 复制代码
<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 中接收
javascript 复制代码
// /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 中推送消息

javascript 复制代码
<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,并不友好
  • 所以不推荐这种写法,后续有封装
javascript 复制代码
<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
javascript 复制代码
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 页面使用
javascript 复制代码
<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>
相关推荐
Python开发吖11 分钟前
【已解决】python的kafka-python包连接kafka报认证失败
开发语言·python·kafka
@老蝴3 小时前
C语言 — 通讯录模拟实现
c语言·开发语言·算法
♚卜卦5 小时前
面向对象 设计模式简述(1.创建型模式)
开发语言·设计模式
安全系统学习5 小时前
网络安全之RCE简单分析
开发语言·python·算法·安全·web安全
Swift社区6 小时前
Swift 解法详解:如何在二叉树中寻找最长连续序列
开发语言·ios·swift
yutian06066 小时前
C# 支持 ToolTip 功能的控件,鼠标悬停弹提示框
开发语言·microsoft·c#
byte轻骑兵7 小时前
【C++特殊工具与技术】优化内存分配(四):定位new表达式、类特定的new、delete表达式
开发语言·c++
chao_7897 小时前
标注工具核心代码解析——class AnnotationVie【canvas.py]
开发语言·python·qt5
YuTaoShao7 小时前
Java八股文——JVM「内存模型篇」
java·开发语言·jvm
广州正荣7 小时前
成绩管理革新者:C++驱动的智能数据处理平台
c++·人工智能·科技