尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Vue电商数据可视化监控大屏源码解析:实时数据流与WebSocket实战

Vue电商数据可视化监控大屏源码解析:实时数据流与WebSocket实战 简介这套基于 Vue 的电商平台数据可视化实时监控系统源码定位于教学与实战结合的轻量级项目适合计算机、数学、电子信息等专业学生用于课程设计、期末大作业或毕业设计参考。项目采用前后端分离思路前端围绕 Vue 构建可视化监控界面后端基于 Koa 框架提供数据接口涵盖数据模拟、中间件处理、接口路由等常见模块。压缩包共含 17 个文件主要由 9 个 JSON 配置文件、6 个 JavaScript 逻辑文件及 1 份 Markdown 项目说明组成整体仅 13KB结构精简清晰便于快速阅读源码和梳理运行流程。已有 300 人学习下载适合具备一定 Vue 基础、希望深入理解电商数据可视化与实时监控系统实现的开发者参考借鉴。资源附带项目说明文档可帮助读者了解目录结构、启动方式与数据组织方式便于二次开发与功能扩展。1. 从零看透 vue 电商数据可视化监控的源码骨架一个典型的电商数据可视化实时监控系统表面上是一张大屏加几组图表真正决定它能不能落到生产环境里的反而是数据从采集到展示的链路是否足够短、足够稳。基于 Vue 实现的这类系统通常把订单、支付、退款、库存这些业务指标从后端接口或消息队列里拉出来再通过 WebSocket 或轮询推到前端经过 ECharts 等可视化库渲染成实时变化的图表。如果你拿到一份这样的源码第一步不是急着读懂大屏上的每一个数字而是先分辨它属于哪种数据流模型是“定时拉取 全量刷新”还是“事件推送 增量更新”这两种模式对代码结构、组件拆分和性能调优的影响完全不同也直接决定了你后续改造成本的高低。源码里最容易让人困惑的往往不是 Vue 组件本身而是数据更新后图表为什么“闪了一下”、为什么 WebSocket 断开后卡片就一直停留在旧数据上、以及同一个接口的数据在多个组件里重复请求这类问题。这些技术点背后都有相对固定的工程解法你要做的就是在源码中找到对应实现并确认它是否够健壮。适合阅读这份源码的人是已经在 Vue 上写过一些中小型项目、但对实时数据可视化缺乏系统实践的开发者本文会沿着数据流这条主线把这类系统的骨架、图表刷新策略、实时通道实现、渲染优化和调试技巧依次拆开让你拿到任何一份同主题源码时都能按同样的思路去读而不是陷在某个组件的细节里出不来。2. 数据流先行Vue 应用结构与实时监控的依赖管理2.1 先从 package.json 和目录结构判断项目成熟度拿到源码第一件事不是打开 App.vue而是打开 package.json 看依赖。一个基于 Vue 的电商数据可视化监控系统依赖列表通常会包含这几个类型核心框架vue、vue-router、pinia 或 vuex、图表库echarts 或 antv、请求库axios、实时通信库socket.io-client 或原生 WebSocket可能还有 mock 数据生成工具和日期处理库。逐一说明这些依赖的作用没有意义关键是看它们的版本组合是否合理Vue 3 项目用 Vue 2 生态的组件、ECharts 4 的语法配合 Vue 3 的响应式系统在实时刷新场景下经常出现 setOption 不更新、图表闪烁等诡异 bug这类问题排查起来极耗时间。目录结构上靠谱的项目一般会遵循“页面 / 组件 / 组合式函数 / 工具 / 接口”这样的分层。以我接触过的多个同类型项目为例常见组织方式是这样的src/ ├── api/ # 接口请求封装 │ ├── modules/order.ts # 订单相关接口 │ └── ws.ts # WebSocket 封装 ├── components/ │ ├── charts/ # 图表组件ECharts 封装 │ └── panel/ # 大屏面板组件 ├── composables/ # Vue 3 组合式函数 │ ├── useRealtimeData.ts │ └── useResize.ts ├── stores/ # Pinia 状态管理 ├── views/ │ ├── dashboard/index.vue # 大屏主页面 └── utils/ # 工具函数这段目录展示了实时监控系统的两个核心特点第一api 模块把普通 HTTP 请求和 WebSocket 分开维护因为两者的生命周期管理完全不同第二组合式函数被用于实时数据订阅这类带有副作用逻辑的场景而不是塞在组件内部。如果你看到的是 Vue 2 项目对应位置应该会有 mixins 目录。项目里若多出 mock 或 mock-server 目录通常意味着源码集成了本地模拟数据方便你跑起来这类项目调试时会省掉很多搭后端的精力优先跑通 mock 再关掉它。2.2 数据状态管理实时数据放 Pinia 还是组件本地实时监控系统的数据状态管理核心争议点在于“这份数据是单组件独占还是共享”。在电商大屏里订单量、销售额这些数据往往会被多个区域同时引用比如顶部指标卡片显示“今日销售额”地图散点图按省份聚合显示同一份订单数据排行榜又展示销售额 TOP10 商品。这种情况下数据放组件本地会导致多处重复建立 WebSocket 连接、重复处理同样的消息既浪费资源又容易造成不同组件间的数据不一致所以集中式状态管理是这类系统最常用的方案。// stores/dashboard.ts import { defineStore } from pinia import { ref } from vue export const useDashboardStore defineStore(dashboard, () { const salesData refSalesItem[]([]) const orderCount ref(0) const lastUpdated refnumber(Date.now()) const connectionStatus refconnected | disconnected(connected) function updateSalesData(data: SalesItem[]) { salesData.value data lastUpdated.value Date.now() } function appendOrderCount(increment: number) { orderCount.value increment } return { salesData, orderCount, lastUpdated, connectionStatus, updateSalesData, appendOrderCount } })上面是 Pinia 组合式写法Option 写法在 Vue 2 项目里更多见逻辑等价。代码要点在于存储层只管“数据的更新动作”不管“数据怎么来的”WebSocket 连接成功后的消息分发应该由独立的调度模块调用 updateSalesData 或 appendOrderCount 方法填充状态组件层只负责读取 store 里的值来渲染。这样做的好处是组件可以任意拆分、重挂载数据不会丢多组件订阅同一份数据也不会产生重复消费。参数与设计取舍lastUpdated 时间戳的目的是在页面上展示数据“多久前更新过”对于实时监控的“心跳感”很关键connectionStatus 用于统一控制断线时的全局遮罩或提示条。是否保留这两个字段建议从自己项目的验收标准出发——如果产品经理只要数据不出错、不要求看到延迟这两个字段可以去掉以精简代码。数据放在 store 后要格外注意不要用store.$patch整体替换一个大对象而是像上面这样精细化更新字段否则 Vue 响应式系统会触发大范围的组件重渲染在实时推送频繁时非常消耗性能。2.3 接口层封装HTTP 轮询与 WebSocket 的分工很多“实时监控”项目实际上不是真实时而是 HTTP 轮询。你要在源码里看清楚这一点如果 WebSocket 相关代码只在网络请求工具里出现一次、而且没有心跳机制那这个系统的实时性大概率是靠设置定时器不断请求/api/dashboard/overview实现的。轮询不是贬义词在数据量不大、接口响应快、实时性要求只有秒级以下时轮询比 WebSocket 更简单也更稳定不会有连接状态问题、不需要额外维护服务端推送网关。// api/polling.ts import axios from axios export async function fetchDashboardSnapshot(): PromiseDashboardSnapshot { const { data } await axios.getDashboardSnapshot(/api/dashboard/snapshot, { timeout: 5000, params: { _t: Date.now() } }) return data } // 调用方每 5 秒拉取一次 setInterval(async () { try { const snapshot await fetchDashboardSnapshot() store.updateSnapshot(snapshot) } catch (err) { console.error(polling snapshot failed:, err) } }, 5000)这段代码中_t: Date.now()是简单有效的缓存穿透处理防止浏览器或中间层对同一 URL 做强缓存导致数据不更新。timeout 设为 5000 毫秒是为了避免接口异常时请求长时间挂起导致定时器堆积上一次还没返回、下一次又发起了形成并发风暴。轮询的定时器建议用 setInterval 还是 setTimeout我一般会用 setTimeout 在请求完成后重新计时因为 setInterval 无法感知上一次请求是否结束接口慢时会出现多处请求重叠。等接入 WebSocket 后轮询可以作为兜底通道保留——主数据走 WebSocket每隔 30 秒再用一次轮询做数据对账实时监控系统里这种“双通道补偿”的做法相当常见能有效防止推送通道静默丢失消息。3. 图表不是配上去的ECharts 与 Vue 组件的刷新策略3.1 把 ECharts 封装成可复用的 Vue 组件很多源码里会有一个base-chart.vue或类似名字的公共组件这个组件几乎决定了整个项目的图表代码优雅程度。封装 ECharts 组件的核心不是塞一个 div 然后echarts.init而是要考虑三个问题初始化后 ECharts 实例存在哪里、组件销毁时实例释放没有、数据更新时 setOption 的开销怎么控制。下面的封装方式在真实项目里很常见也是我认为 Vue 3 ECharts 5 下最稳妥的写法template div refchartRef classchart-container/div /template script setup langts import { onMounted, onBeforeUnmount, ref, watch } from vue import * as echarts from echarts const props defineProps{ option: EChartsOption renderer?: canvas | svg }() const chartRef refHTMLDivElement | null(null) let chart: echarts.ECharts | null null onMounted(() { if (!chartRef.value) return chart echarts.init(chartRef.value, undefined, { renderer: props.renderer || canvas }) chart.setOption(props.option) const resizeObserver new ResizeObserver(() chart?.resize()) resizeObserver.observe(chartRef.value) }) watch(() props.option, (newOption) { if (!chart) return chart.setOption(newOption, { notMerge: true }) }, { deep: false }) onBeforeUnmount(() { chart?.dispose() chart null }) /script代码里的关键参数说明notMerge: true表示每次更新时整体替换数据而不是与旧数据做浅合并——实时监控里如果后端返回的数据结构是完整快照用 notMerge 可以避免已下线的商品仍残留在图上如果后端只返回增量数据如只更新某根柱子的值则应设置notMerge: false并配合replaceMerge精确指定要替换的系列。renderer 参数决定图表用 canvas 还是 svg 渲染监控大屏默认 canvas因为数据点多时 canvas 性能更好如果页面上图表数量多但每个数据点少svg 的交互体验会更细腻。3.2 setOption 的频率与数据合并陷阱实时监控系统里最常见的图表性能问题不是因为 ECharts 慢而是因为 setOption 被高频率调用了很多次。让我把这里的机制讲清楚ECharts 在 canvas 渲染模式下每调用一次 setOption 并触发内部更新浏览器就会经历一次完整的重绘流程如果推送频率是每秒 10 条消息、页面上有 10 个图表那意味着每秒要重绘 100 次再加上 Vue 组件本身的响应式更新帧率下降、CPU 飙升都是必然结果。处理方案是节流 批量收集数据// 数据缓冲队列每 500ms 统一刷新一次图表 const bufferQueue: RealtimeMetric[] [] let flushTimer: number | null null function pushRealtimeMetric(metric: RealtimeMetric) { bufferQueue.push(metric) if (flushTimer ! null) return flushTimer window.setTimeout(() { const batched bufferQueue.splice(0, bufferQueue.length) updateChartsWithBatch(batched) flushTimer null }, 500) }这段逻辑的做法是新数据到达时先进入队列不立即触发图表更新等到 500 毫秒的窗口结束把这一段时间内的所有数据点合并成一次 setOption 调用。500 毫秒是兼顾“实时感”和“渲染成本”的经验值不是标准答案。如果你的监控指标是订单量这类秒级波动不明显的业务可以放宽到 1000 毫秒如果是交易额这种对数值跳变敏感、产品要求“看起来在跳动”的指标200 到 300 毫秒比较合适。批量更新时要注意折线图的时间字段对齐一次推送里若包含多个时间点要在 series 的 data 里按序列追加而不是每个时间点都触发一次图表重绘。3.3 大屏常见的图表类型与数据粒度抉择这个监控系统的页面上通常有这几类核心图表订单趋势折线图、省份销售分布地图、类目占比饼图、实时交易滚动列表、以及一些自定义的仪表盘或数字翻牌器。图表类型的选择要跟数据粒度匹配这是源码阅读时值得多琢磨的地方比如折线图的 X 轴时间粒度是分钟还是小时直接决定了后端推送频率和前端缓存策略。粒度过小比如秒级折线图上的点会非常密集需要开启 ECharts 的sampling: lttb来降采样粒度过大图表的“实时感”会大打折扣。lineSeries: { name: 订单量, type: line, smooth: true, showSymbol: false, sampling: lttb, data: ordersTrendData }sampling: lttb是 ECharts 内置的 Largest-Triangle-Three-Buckets 算法它会从密集数据点中抽取一小部分代表点来绘制折线视觉上几乎看不出差异但渲染性能能提升一个数量级。对于电商监控系统订单趋势线用 LTTB 降采样后仍然能保持波峰波谷的形状业务上足够用。饼图和排行榜这类非时序数据则不太需要采样它们更需要注意的是数据更新时不要把整个 option 替换掉而是通过legend和series[0].data的局部更新来减少开销。4. WebSocket 才是实时性的底线连接方案与断线恢复4.1 一个生产可用的 WebSocket 封装要处理几件小事电商数据可视化实时监控系统里WebSocket 承载的是订单实时流入、异常告警、退款通知这些需要主动推送的数据。写 WebSocket 封装很多新手只会new WebSocket(url)然后在 onmessage 里更新数据但一个生产环境的连接管理远不止这些心跳保活、断线重连、消息确认/重发、连接状态广播、避免重复订阅少了任何一环系统跑上几小时后就会出现“图表不更新了但页面没报错”的隐蔽故障。下面这段代码是这类封装里我认为比较扎实的最小实现消息确认部分做了简化但逻辑完整// api/realtime.ts type MessageHandler (data: any) void class RealtimeClient { private ws: WebSocket | null null private handlers: Mapstring, MessageHandler new Map() private heartbeatTimer: number | null null private reconnectAttempts 0 private readonly maxReconnectAttempts 10 private readonly baseDelay 1000 private manualClosed false connect(url: string) { this.manualClosed false this.reconnectAttempts 0 this.establish(url) } private establish(url: string) { this.ws new WebSocket(url) this.ws.onopen () { this.reconnectAttempts 0 this.startHeartbeat() // 连接建立后通知业务层清理断线状态 store.connectionStatus connected } this.ws.onmessage (event: MessageEvent) { const frame JSON.parse(event.data) if (frame.event this.handlers.has(frame.event)) { this.handlers.get(frame.event)!(frame.payload) } } this.ws.onclose () { this.stopHeartbeat() if (this.manualClosed) return this.reconnect(url) } this.ws.onerror () { // onerror 后必然触发 onclose这里只需要记录日志 console.error([realtime] websocket error, will reconnect soon) } } private reconnect(url: string) { if (this.reconnectAttempts this.maxReconnectAttempts) { console.error([realtime] max reconnect attempts reached, give up) return } const delay this.baseDelay * Math.pow(2, this.reconnectAttempts) setTimeout(() { this.reconnectAttempts 1 this.establish(url) }, delay) } private startHeartbeat() { this.heartbeatTimer window.setInterval(() { if (this.ws this.ws.readyState WebSocket.OPEN) { this.ws.send(JSON.stringify({ event: ping, ts: Date.now() })) } }, 30000) } private stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer) this.heartbeatTimer null } } on(event: string, handler: MessageHandler) { this.handlers.set(event, handler) } close() { this.manualClosed true this.stopHeartbeat() this.ws?.close() } } export const realtimeClient new RealtimeClient()这段代码表面上是连接管理但值得展开说明的有三个参数心跳间隔 30 秒这是服务器端常见的心跳超时阈值的一半左右原因是很多网关Nginx 代理、云负载均衡等的空闲连接超时设置在 60 秒上下客户端 30 秒发一次 ping 包可以保证连接不被中间层断开重连退避指数基数 1000 毫秒第一次断线后 1 秒重试、第二次 2 秒、第三次 4 秒最多 10 次合理的指数退避可以避免服务端重启时客户端瞬间发起大量连接手动关闭标记 manualClosed否则业务代码正常退出时也会触发重连逻辑导致页面销毁后连接还在后台反复建立这在单页应用里非常容易踩到。4.2 消息分发与数据增量合并策略WebSocket 收到的原始数据通常不是你页面上要展示的样子。比如后端推送的是一条新订单事件{ orderId, amount, province, category, timestamp }但你要的是“当前销售额累计值”和“省份分布图的新增点”。在源码里你会看到两种处理路径一种是在 onmessage 里直接更新 store简单直接但逻辑耦合另一种是引入事件总线的思路把消息统一转给一个 dispatcher由它决定分发给哪些 store 或组件。后者在监控系统里更合适因为一条订单消息往往需要同时更新多个看板区域。// handlers/eventDispatcher.ts import { useDashboardStore } from /stores/dashboard import { useOrderStore } from /stores/order export function dispatchRealtimeEvent(frame: RealtimeFrame) { const dashboardStore useDashboardStore() const orderStore useOrderStore() switch (frame.event) { case ORDER_CREATED: orderStore.appendOrder(frame.payload) dashboardStore.incrementOrderCount(1) dashboardStore.addSalesAmount(frame.payload.amount) break case REFUND_APPROVED: dashboardStore.incrementRefundCount(1) dashboardStore.decrementSalesAmount(frame.payload.amount) break default: break } }这里的 switch-case 看起来朴素但在实时监控项目里比写一堆链式调用更容易排查问题某天订单量不对时你只需要在 dispatchRealtimeEvent 打个断点看事件有没有到达、分发到了哪个分支就能快速判断是推送丢失还是状态更新逻辑写错。这段代码还隐含了一个重要设计原则状态更新要“即来即算”尽量不依赖旧状态做复杂递归计算。比如销售额累计直接dashboardStore.addSalesAmount(frame.payload.amount)在 store 内部维护一个 total 字段即可不要每次从订单数组里去 reduce。4.3 断线期间的数据对账用一次快照拉齐状态断线重连成功后客户端和服务器之间的数据必然存在缺口断线那几十秒里产生的订单没有推送过来如果直接继续依赖增量更新图表上的数据就会永久性少一段。常见的可靠解法是重连成功后主动请求一次全量快照把 store 整体重置。ws.onclose () { this.stopHeartbeat() if (this.manualClosed) return this.reconnect(url) } // 在 onopen 里追加快照拉取逻辑 this.ws.onopen async () { this.reconnectAttempts 0 this.startHeartbeat() store.connectionStatus connected // 重连后拉取全量快照避免增量缺口 const snapshot await fetchDashboardSnapshot() store.resetWithSnapshot(snapshot) }这段代码把“WebSocket 连接建立”和“数据对账”绑定在了一起无论是首次连接还是断线重连只要 onopen 触发就拉一次 HTTP 全量快照来重置页面。这样做的代价是接口压力比纯推送大一些但换来的是简洁明确的一致性保障对于电商监控这种数据准确性要求高的场景值得付出这点额外请求。如果把快照拉取放在“重连超过 N 次才做”代码会复杂且容易漏。我一般会在自己的项目里保留这个“每次 onopen 都拉快照”的简单策略因为任何时候你都很难证明哪些增量消息丢了而快照是唯一的真相来源。5. 数据大屏的渲染瓶颈与工程化取舍5.1 组件渲染层级太深怎么办用 shallowRef 切断响应式链实时监控系统的页面组件层级往往比较深大屏页面 - 指标卡片 - 图表组件 - 图表数据对象。如果图表的数据对象是一个多层嵌套的 JSONVue 会在每次数据更新时递归地对所有层级做响应式处理这笔开销在 push 频率高时会变得不可忽视。Vue 3 中处理这类“大量频繁变更但不需要深层响应式”的数据最直接的手段是 shallowRef。import { shallowRef } from vue const chartOption shallowRef({ ...initialOption }) function updateChartData(seriesData: number[]) { // 只替换 series.data整个对象结构不触发深层响应式 chartOption.value { ...chartOption.value, series: [{ ...chartOption.value.series[0], data: seriesData }] } }浅层响应式的含义是Vue 只追踪chartOption.value这个引用是否发生变化而不追踪它内部嵌套对象的属性变化。所以上面代码必须整体重建 chartOption 的值通过扩展运算符生成新对象而不能用chartOption.value.series[0].data seriesData这种原地修改——原地修改不会触发更新。这个组合shallowRef 整体替换在数据大屏里非常好用因为图表组件一般只关心“我给 ECharts 的 option 变了没有”并不关心从根到叶每层的属性级依赖Vue 省掉深层响应式后渲染开销能下降 30% 以上。5.2 ECharts 的按需引入与初始动画陷阱基于 Vue 的电商监控项目里如果你在 main.ts 里写import * as echarts from echarts最终 bundle 体积会非常可观ECharts 完整包超过 1MB。监控大屏虽然有网络带宽余量但源码给你看的是工程化习惯。按需引入的核心是只加载你用到的图表和组件ECharts 5 的 tree-shaking 支持做得很好推荐用下面这种方式// utils/echarts.ts import { use } from echarts/core import { CanvasRenderer } from echarts/renderers import { LineChart, BarChart, PieChart } from echarts/charts import { GridComponent, TooltipComponent, LegendComponent, DataZoomComponent } from echarts/components use([ CanvasRenderer, LineChart, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent ]) import type { EChartsOption } from echarts export type { EChartsOption }这段代码只注册了折线图、柱状图、饼图和对应的 Grid/提示框/图例/区域缩放组件。如果你的页面还需要地图散点图之类的视觉要在 use 里额外加MapChart与GeoComponent否则运行时会出现“series.type 不能为空”之类的报错。按需引入在开发环境会带来一个体感问题因为要手动维护 use 列表新增图表类型时容易忘记注册所以源码里通常会把 echarts.ts 单独剥离出来并在文件头部用注释说明当前注册了哪些图表方便后来者快速补充。初始动画陷阱指的是 ECharts 默认在 setOption 第一次渲染时播放动画比如柱状图从零长到目标值在实时监控场景里数据每次推送都触发动画会让画面显得很乱。需要在系列配置里显式关掉动画或大幅缩短动画时长lineSeries: { type: line, animation: false, animationDurationUpdate: 150, animationEasingUpdate: linear }更新时保留 150 毫秒的缓动是为了让折线移动有平滑过渡不至于跳变但第一次初始化的动画animation: false直接关闭避免大屏加载时所有图表同时做入场动画、白屏时间过长。5.3 空间换时间虚拟滚动与卡片组件 lazy 加载监控大屏页面最底部一般会有一个滚动的“实时订单列表”新订单从底部挤入、旧订单从顶部淡出。这个列表如果用 v-for 直接渲染全部订单数据页面上 DOM 节点数会持续增加越跑越卡。一个成熟的源码里实时列表一定会处理“节点上限”这个约束。const displayOrders refOrderItem[]([]) function feedOrder(order: OrderItem) { if (displayOrders.value.length 50) { // 超出上限时从头部移出最旧的一条 displayOrders.value.shift() } displayOrders.value.push(order) }上限 50 条是一个常见的配置经验值具体数字取决于你的设备性能普通办公电脑每屏能流畅渲染的 DOM 节点数大约在 100 到 200 之间考虑到大屏页面同时有多个图表在重绘把列表长度控制在 30 到 50 条比较稳妥。业务上你也只需要展示最近一段时间的新增订单没人会去大屏上翻历史数据。这个思路的本质是“空间换时间”限制数据规模而不是优化每个节点的渲染效率对比虚拟滚动库来说更简单、更容易在源码里被理解。对于大屏上的多个布局区块顶部指标栏、中间地图、右侧排行榜如果一次性全部渲染打开页面的首帧渲染压力会很大。常见做法是用 Vue 的异步组件配合 defineAsyncComponent让非首屏区域的组件在空闲时再加载import { defineAsyncComponent } from vue const SalesRankingPanel defineAsyncComponent(() import(/components/panel/SalesRankingPanel.vue) )主页面模板里正常使用SalesRankingPanel /它不会阻塞大屏主体内容的渲染。注意异步组件在 WebSocket 消息到达时才加载完成会出现短暂留白所以这类组件适合放在“数据更新不频繁”或“加载完成前有骨架屏占位”的场景比如排行榜面板几秒才刷一次不适合用在高频更新的核心图表上。5.4 开发环境通过 vite 配置代理联调后端接口拿到源码后你肯定想先跑起来。实时监控系统的前端页面通常长这样登录页、大屏主页、数据管理页接口地址可能是http://10.20.15.100:8080/api/*但你本地开发时不想配置真实的服务器地址或者只想先看看静态效果。Vite 项目的开发代理配置就在 vite.config.ts 里这是源码调试路上绕不过去的一环。// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://10.20.15.100:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ), timeout: 5000 }, /ws: { target: ws://10.20.15.100:8080, ws: true, timeout: 10000 } } } })注意 WebSocket 的代理路径通常是/ws前缀和 HTTP 的/api分开配置ws: true是开启 WebSocket 代理的关键选项。rewrite 函数是否删掉前缀取决于后端接口本身带不带/api上下文后端如果本来就是http://host:8080/api/dashboard那你就不该 rewrite后端如果路由是http://host:8080/dashboard才需要把/api前缀剥掉。这个细节非常容易踩坑代理配完发现 404 或 502先检查 rewrite。6. 拿到源码后先改这三处再谈业务扩展6.1 把写死的配置抽到 .env 文件里实时监控系统往往要对接多个环境本地开发环境、测试环境、生产环境。源码里如果直接把 WebSocket 地址和 HTTP 接口地址写成字符串常量换环境就要改代码重新编译。按环境区分配置是工程化最基本的要求常见的做法是在项目根目录建.env.development和.env.production两个文件里面放好变量然后在代码里读取。# .env.development VITE_API_BASE_URL/api VITE_WS_URLws://localhost:8080/ws # .env.production VITE_API_BASE_URLhttps://monitor.example.com/api VITE_WS_URLwss://monitor.example.com/ws代码中通过import.meta.env.VITE_API_BASE_URL读取变量。注意 Vite 项目环境变量必须以VITE_前缀开头否则不会被暴露到客户端代码里面。抽完这一层之后项目从源码包里的 mock 环境切到真实环境只需要改环境文件而不用动业务逻辑。6.2 把实时数据订阅逻辑封装成 useRealtime composable在监控项目里一个页面可能会订阅多个事件订单事件、退款事件、库存事件、系统告警事件。如果每个页面都自己调用realtimeClient.on(ORDER_CREATED, callback)页面卸载时容易忘记取消订阅导致回调函数被重复注册、同一个事件触发多次更新。封装一个 composable 来统一处理订阅和取消订阅是消除这类故障的干净做法// composables/useRealtime.ts import { onMounted, onBeforeUnmount } from vue export function useRealtimeT( eventName: string, handler: (payload: T) void ) { const wrappedHandler (payload: T) handler(payload) onMounted(() { realtimeClient.on(eventName, wrappedHandler) }) onBeforeUnmount(() { realtimeClient.removeHandler(eventName, wrappedHandler) }) return { connected: realtimeClient.isConnected() } }realtimeClient.removeHandler是前面封装里没有展开的方法你需要在 RealtimeClient 类中为 handlers Map 增加一个 delete 操作。这样设计之后业务组件里只需要写一行useRealtime(ORDER_CREATED, handleNewOrder)Vue 生命周期自动帮你把订阅和释放管理起来代码量也减少了很多。6.3 利用页面可见性 API 自动处理后台标签页的暂停与恢复实时监控大屏挂在监控室显示器上一般不切页但如果有人把页面切到后台标签页浏览器会降低定时器和动画的调度频率WebSocket 消息到达后图表更新可能滞后甚至卡顿。一个实用的做法是监听visibilitychange事件在页面重新可见时立即拉一次快照并重置图表状态document.addEventListener(visibilitychange, () { if (document.visibilityState visible) { fetchDashboardSnapshot().then((snapshot) { store.resetWithSnapshot(snapshot) }) } })这段代码补上了“标签页被浏览器冻结期间错过数据”的兜底逻辑。你可能会问WebSocket 本身不是有消息缓存吗但浏览器为了节省资源确实可能延迟处理后台标签页的 WebSocket 消息对于实时监控这种对数据时效性敏感的场景等页面可见时靠快照对齐一次数据比等推送队列慢慢消化要可靠得多。验证这个逻辑是否有效的办法把页面切到后台等一分钟再切回来观察监控大屏上的“最后更新时间”是否立刻变成最近时间而不是停留在切换前的某个旧时间点。本文还有配套的精品资源点击获取
返回列表