WebSocket实时推送+ HTTP 定时轮询:一块大屏两套取数怎么不打架

WebSocket 实时推送 + HTTP 定时轮询:一块大屏两套取数怎么不打架

一块典型的工业监控大屏,数据来源天生就是两种节奏的:

设备遥测、实时告警,要求秒级甚至亚秒级到达,错过一帧就是错过一次故障------这必须是服务端主动推的;能耗汇总、班次统计、日累计值,一分钟更新一次都嫌快,用推送纯属浪费连接------这适合客户端定时拉。

于是问题来了:两条链路谁先谁后、断线了谁兜底、组件销毁时谁负责清理?处理不好,轻则数据闪烁,重则「断线一次、整屏数据全停」。

▲ 左边是把所有数据都压进一条长连接,右边是按节奏分工、出口统一

这篇讲我们怎么在这道题上落地。结构、字段名和配置片段来自对自家大屏运行器 datavis-render-vue3 的勘察记录(对应的两个核心文件是 src/packages/datavisRender/hooks/useApiRequest.ts 与 useApiWebsocket.ts),只讲机制,不涉及业务数据。

一、两套取数,在图纸里就是两个并列的配置项

先看它们在数据里长什么样。一份大屏图纸(designData)的 config 段里,interfaceApi 下面并列挂着 api 和 websocket 两个配置块(字段名照勘察文档的核心数据结构表):

json 复制代码
{
  "config": {
    "width": 1920, "height": 1080,
    "renderMode": "fit",
    "defaultScreenId": "screen_001",
    "interfaceApi": {
      "api": {
        "enable": true, "url": "...", "method": "GET",
        "headerFormat": "...", "paramsFormat": "...",
        "resFormat": "...", "timeInterval": 10
      },
      "websocket": {
        "enable": false, "url": "...",
        "paramsFormat": "...", "resFormat": "...", "sendScript": "..."
      }
    }
  },
  "backend": [{ "objects": [...] }],
  "screen": [
    { "uid": "screen_001", "objects": [...] },
    { "uid": "screen_002", "objects": [...] }
  ],
  "frontend": [{ "objects": [...] }]
}

这个结构传递了三个设计决策:

第一,两者对等、互不隶属。 api 和 websocket 是兄弟节点,各自有独立的 enable 开关。不存在「WebSocket 为主、HTTP 兜底」的主从关系------两套链路可以同时开,也可以只开一个,图纸层面不预设谁是主角。

第二,差异收敛在配置字段上。 HTTP 侧多了 method、headerFormat 和 timeInterval(轮询间隔);WebSocket 侧多了 sendScript(连接建立后要发的订阅消息)。除此之外,两边共用同一对脚本字段 paramsFormat / resFormat------请求怎么组装、响应怎么过滤,两套链路的语义是一致的。

第三,配置跟着图纸走。 这份 JSON 会随图纸一起序列化保存,「这块屏用不用推送、轮询几秒一次」是大屏的一部分,而不是运行环境的私事。

二、HTTP 轮询:定时器的单例与清理

HTTP 这条链路由 useApiRequest 承担。子屏挂载时调用 sendRequest,完整走一遍是六步:

text 复制代码
① 组装     headerFormat / paramsFormat 脚本生成请求头与参数
② 发起     visApi.axios 发出请求
③ 过滤     响应交给 resFormat 脚本加工
④ 通知     editor.fire('apiRequest', result)
⑤ 派发     运行器监听到事件,exposeSetData(result) 分发数据
⑥ 循环     timeInterval > 0 时 setInterval 定时重跑 ①-④

▲ 定时器不是随手 new 的,它挂在固定位置、有明确的清理时机

这六步里真正值得展开的是两处。

脚本三件套是字符串,不是函数。 headerFormat / paramsFormat / resFormat 在配置里都是脚本字符串,运行时经 visApi.funRunScript 转成函数执行。这么做换来的表达能力是:脚本上下文里能拿到 editor 实例,还有注入的 lodash、dayjs、axios------「把当前屏的 uid 拼进查询参数」「把毫秒时间戳格式化成班次」这类逻辑,用户在配置界面里写几行脚本就能完成,不用改运行器代码。执行引擎还有个容错版本 tryScript,脚本出错不至于把整条取数链路带崩。

定时器挂在 editor.__apiRequestIntervalTimer 上保证单例。 这是个不起眼但很关键的细节:子屏会切换、组件会重复挂载,如果每次挂载都裸起一个 setInterval,切换十次屏就有十个定时器在同时请求,接口压力翻倍、数据还会互相覆盖。挂到 editor 的固定属性上,新一次请求前先清掉旧定时器,全局永远只有一个在跑。对应的清理时机也明确:组件销毁时 onBeforeUnmount 里 clearInterval。

还有一个容易被忽略的语义:接口数据是全局的 。响应结果不属于任何单个组件,所有组件共享这一份数据,每个组件通过自己身上的 signal 字段(如 "device.temperature")从全局数据里按路径取自己需要的那一片。这个设计后面还会讲到它的好处。

三、WebSocket 推送:持久连接的生命周期

WebSocket 这条链路由 useApiWebsocket 承担,sendWebsocket 负责建立连接,五步:

text 复制代码
① 建连     创建 WebSocket 实例,挂到 editor.__websocketInstance 保证单例
② 组参数   paramsFormat 脚本生成连接参数,拼接到 URL(支持对象/字符串两种格式)
③ 订阅     onopen 时执行 sendScript 脚本(如发送订阅消息)
④ 收数     onmessage 时用 resFormat 脚本过滤,editor.fire('websocketRequest', result)
⑤ 清理     组件销毁时关闭 WebSocket 连接

和 HTTP 链路对着看,结构几乎是镜像的:同样的脚本三件套语义、同样的「过滤后 fire 事件」出口、同样的销毁清理。差异只有两点------连接是持久的,以及推送是服务端主导的 :HTTP 链路的节奏由客户端 timeInterval 决定,WebSocket 链路里客户端只在 onopen 时喊一嗓子(sendScript 发订阅),之后什么时候来数据、来多少,全由服务端说了算。

这个差异决定了它们适合的数据形态不同:轮询适合「我要的时候去拿」的汇总类数据,推送适合「你不知道它什么时候来」的事件类数据------设备告警、状态跳变就是典型。

四、不打架的关键:两个出口,汇成一个入口

前面一直在说 editor.fire(...)。两条链路各发各的事件,为什么组件不会乱?

看运行器的数据流转全貌(图来自勘察文档的模块关系章节):

text 复制代码
┌────────┐   ┌──────────┐   ┌────────┐
│useApi  │   │useApi    │   │useApi  │
│Request │   │Websocket │   │Script  │
│(HTTP)  │   │(WS)      │   │(脚本)  │
└───┬────┘   └────┬─────┘   └────┬───┘
    │  fire('apiRequest'/'websocketRequest')
    └───────────┬────────────┘
                ▼  exposeSetData(result)
    ┌───────────────────────────┐
    │  按 signal 路径派发数据     │
    │  node.emitter.emit('setData', itemData) │
    └───────────┬───────────────┘
                ▼
    组件监听 'setData' 事件刷新

▲ 源头可以有很多条,出口只有一个,组件只认 setData

分工在源头,统一在出口 ------这就是两套取数不打架的答案。运行器主组件 index.vue 里的 handleEventlisteners 统一监听 apiRequest、websocketRequest 这些事件,无论数据是拉来的还是推来的,最后都走同一个 exposeSetData(result),按 signal 路径找到目标组件,经 node.emitter.emit('setData', itemData) 送过去。

对组件来说,世界被简化成了一件事:只认 setData 事件,不关心、也不需要关心数据是从哪条链路来的。同一个图表组件,上一秒吃轮询数据,下一秒吃推送数据,渲染逻辑一行不改。

顺带一提,其实还有第三条腿:useApiScript(脚本数据源),不走网络、本地脚本直接产数,常用于联调阶段的模拟数据。三条 hooks 全部遵循同一个「fire 事件 → 统一派发」的契约。新增一种数据源类型,只要它遵守这个契约,整套组件库一行不用改。

五、图纸里那些字段,在编辑器里长什么样

第一节贴了 config.interfaceApi 的 JSON,第二节说脚本三件套是字符串------这些字段不是凭空写的,它们是用户在编辑器里填出来的。两张真实编辑器截图对着看,字段与界面的对应关系一目了然。

▲ 新增数据变量:类型选「API 数据」,URL 与请求方法填进图纸的 interfaceApi.api,「定时执行」就是 timeInterval,「数据处理」里写的脚本参数带着 editor

第一张图是数据源配置弹窗:类型选「API 数据」,URL 地址、请求方法(GET)对应图纸里的 url / method,「定时执行」一栏填的数字就是 timeInterval(0 表示不轮询)。下方「数据处理」tab 里是那段 executeScript 脚本,注释写得很直白:data 是接口返回的原始数据,editor 是编辑器核心对象 ------第二节说脚本上下文里能拿到 editor 实例,出处就在这里。

▲ 字段映射:脚本产出的 product / power / reduction 分别接到图表的 x1 / y1 / y2,三组数据驱动同一张柱状图

第二张图是选中一张柱状图后的数据面板:左侧「数据处理」编辑函数产出多组序列,映射表把它接到图表的取值 key 上------x1 对应 product(时间)、y1 对应 power、y2 对应 reduction,三组数据同时驱动同一张图;右侧的 JSON 树就是接口返回的原始结构,monthChart.list 里每一项的 createTime / power / reduction 与映射一一对应。组件身上的 signal 取值路径,在界面上对应「变量」这一栏。

调试这件事也在这层:运行器里有一个 debuggerToolDialog(数据调试器),API / Script / Websocket 三种数据源各有一个调试入口------接口通不通、脚本算出来是什么、推送帧长什么样,编辑阶段就能直接看到,不用等到大屏上线再猜。

六、四条踩坑经验(经验总结)

以下是我们真实项目里撞出来的,属于经验总结,不是勘察文档的结论:

第一,别把所有数据都塞进推送。 看起来「一条 WebSocket 全解决」很优雅,代价是这条连接成了单点:断线一次,连那些一分钟更新一次完全够用的汇总数据也一起停。按数据节奏分工,故障半径小一半。

第二,重连策略要在宿主层做。 运行器的契约是「销毁时关闭连接」,连接断开后的恢复(指数退避、重连后重新发 sendScript 订阅)不由运行器代劳。集成方要自己想清楚:大屏是 7×24 挂着的,网络抖动不是「会不会」而是「每天几次」。

第三,轮询驱动不了动画。 timeInterval 是数据刷新节奏,不是渲染节奏。拿轮询间隔当动画节拍用,接口一抖整个屏的动效跟着卡。数据归数据,渲染归渲染,renderMode 解决的是画面适配,不替你兜数据频率的底。

第四,两套数据的时间基准要提前统一。 轮询数据带的是接口返回时刻的时间戳,推送数据带的是事件发生时刻的时间戳,两者可能差好几秒。同一块屏上「当前值」和「累计值」各用各的时钟,跨系统对账时就会发现数字对不上。汇总展示前,先定一个统一的时间口径。

七、技术总结

回到标题那道题:两套取数怎么不打架?

答案分三层。图纸层 :config.interfaceApi 里 api 与 websocket 并列配置、各自 enable,差异收敛在 timeInterval 与 sendScript 两个字段上。运行器层 :useApiRequest 与 useApiWebsocket 两条 hooks 镜像对称,各自管好自己的定时器单例(__apiRequestIntervalTimer)与连接单例(__websocketInstance),销毁时各自清理。派发层 :两条链路都终结于 editor.fire → exposeSetData → 按 signal 路径派发,组件只认 node.emitter 上的 setData 事件。

架构上最值钱的决策是最后一个:把「数据从哪来」和「数据怎么用」彻底分开。 源头随便加、出口永不变,两套取数才能长期共处而不互相渗透------这比任何防冲突技巧都管用。

相关推荐
Eider19981 小时前
Python基础知识之并发相关-2
开发语言·前端·python
涛声依旧-底层原理研究所1 小时前
AI Agent 运行逻辑深度解析:从感知层到工具调用,再到真实产物
人工智能·架构
用户69371750013841 小时前
AI 能写代码,但工程架构还是应该由人来定
前端·后端
Hilaku1 小时前
前端的技术判断力是怎么练出来的?
前端·javascript·程序员
悠仁さん1 小时前
【C++】set map 使用
java·前端·c++
用户093671755411 小时前
用 4500 行 TypeScript 复刻了童年方格纸上的海战棋
前端·vue.js
CCIE-Yasuo2 小时前
网络安全-学习web开发-基础概念、常用标签
前端·学习
凉介川2 小时前
给 DeepSeek Harness 写插件:按扩展名接管 Windows 右键打开
前端
涛涛ing2 小时前
Pretext之后,2026年的前端正在被“平台”和“AI”同时重写
前端