React+antd实战:环境监测网站实时数据与可视化开发
简介基于 Reactantd 的环境监测网站课程设计项目采用 B/S 架构面向 Web 前端学习者与需要完成课程设计的同学。系统围绕环境数据采集与展示支持数据上传、查看与分析并划分数据分析、数据监控、用户工作台、财务管理、项目管理和人员管理等板块覆盖环境数据实时监控、任务处理、项目财务分配与回收等应用场景。包体共 98 个文件以 jsx、css、js 等前端源码为主搭配多张 png 界面截图、webpack 配置与 Markdown 笔记另含一份毕业论文文档整体压缩包约 3.96MB目录结构清晰便于按模块查阅与二次开发。项目采用自定义 mock 数据并根据 JSON 格式自动生成模拟数据在页面不重启的情况下可保存操作状态配合 AJAX、HTML5、CSS3 等技术体现了完整的 Reactantd 开发流程。已有 176 人学习下载适合希望快速上手 React 生态并落地环境监测类管理系统的开发者参考。1. 环境监测网站不只是「画几条曲线」做环境监测类 Web 项目第一反应往往是画几个折线图、摆几张污染指标卡片。真正接手后才发现难点根本不在图表库调色上而是实时数据怎么低延迟落地、历史分钟数据怎么在页面上不卡顿地翻页、告警状态怎么在表格、指标卡、消息中心三处同步。常见的数据来源是厂区传感器、机房温湿度仪、楼宇新风系统的采集网关数据量看起来不大但一个站点 20 类指标、每 5 秒一条记录一周就是 240 万条前端稍不克制就能把浏览器拖垮。这套「基于 Reactantd 的环境监测网站」要解决的就是从数据接入、状态分发、可视化到表格联动的一整条链路。下面按一线交付会走的路把选型、参数和关键坑位一次说清楚。2. 数据链路设计先定接入层再画页面2.1 环境监测数据源为什么不统一2.1.1 三类常见来源环境监测项目涉及的传感器种类比较多。实际项目里常见的来源有三类一类是 Modbus RTU 温湿度变送器走 DTU 透传网关定时吐字节流一类是空气质量监测仪内置 NB-IoT 模块上报周期性的 JSON 包还有一类是采集软件已经做了窄表落库只开放 HTTP 查询接口。它们的共性是格式各不相同、时间戳各自为政、量纲随时变。如果页面开发一开始就直接消费上游字段后期每换一次硬件厂商就要改一遍组件很容易失控。2.1.2 先做一个字段映射层最常见的做法是在前端放一个适配层把上游消息归一化成统一结构再交给状态库和图表。这样页面永远只认一种形状的数据适配成本被全部收敛在一个目录里。// adapter.ts export interface SensorData { stationId: string; stationName: string; metric: string; // pm25 | temp | humidity | noise value: number; unit: string; ts: number; // 标准化后的 Unix 毫秒时间戳 quality: good | warn | alarm; } export function normalizePayload(raw: Recordstring, unknown): SensorData { // 不同厂商上报字段有的是 value有的是 data.val甚至有字符串 23.5 const val raw[value] ?? (raw[data] as any)?.val ?? parseFloat(String(raw[v] ?? 0)); return { stationId: String(raw[station] ?? raw[devId] ?? unknown), stationName: String(raw[name] ?? raw[stationName] ?? --), metric: String(raw[metric] ?? raw[type] ?? unknown), value: Number(val), unit: String(raw[unit] ?? ), ts: Number(raw[ts] ?? raw[time] ?? Date.now()), quality: judgeQuality(Number(val), raw[metric] as string), }; }这段映射解决三件事字段名不统一value/data.val/v、时间戳格式不统一秒/毫秒/字符串、数值类型不统一字符串数字需要parseFloat。judgeQuality是业务阈值判断不要在组件里散落否则后期调阈值要翻遍所有页面文件。2.2 实时通道轮询还是 WebSocket选实时通道时一个常见误区是无脑上 WebSocket。对环境监测、尤其是厂区这种几十个测点的小规模场景HTTP 轮询实现简单、日志排查方便压力也完全扛得住。下面对比一下维度短轮询setInterval fetchWebSocket 长连接延迟等于轮询间隔通常 5~30s秒级以内服务端改动普通 REST API 即可需要维护长连接网关断线恢复天然恢复无状态需要心跳、重连、补发协议适合规模测点少、告警链路简单上千测点、需要主动推送项目成本低较高如果业务对实时性要求是「1 分钟内有告警」轮询就够了如果要求「5 秒内感知超标」才值得上 WebSocket。实际项目里最常选的做法是主链路用轮询兜底WebSocket 只做告警推送。提示不管选哪种前端都要预留「补拉最近 N 条」的接口。长连接断线期间的数据只有靠重连后的批量补齐才能保证现场完整。2.3 历史数据的分页策略历史查询是环境监测网站上最容易卡死表格的场景。常见做法是服务端做「时间范围 测点 指标」的联合分页前端不做任何数据聚合直接把pageSize控制在 20~50 行避免一次性渲染数万行 DOM。接口一般长这样GET /api/history?stationIdxxxmetricpm25start...end...page1pageSize50图表上要展示一小时内的逐分钟曲线时前端需要把这些分页数据合并成一条时间序列。合并前先按ts升序排序再做重复时间戳去重不要盲目信任服务端返回顺序曾经遇到过同一站点两个采集通道各自上报、时间戳交错的情况不排序直接画折线图像会来回穿针。3. 实时状态管理zustand 与订阅隔离3.1 为什么不用 Context 分发实时数据环境监测页面往往有指标卡、折线图、表格、告警角标同时需要同一份实时数据。用 React Context 的话只要 store 里任意一个传感器的值变化所有useContext的组件都会重渲染几十个卡片一起刷新页面肉眼可见地掉帧。这个场景会用 zustand把「高频变化的数据」和「偶尔变化的元数据」拆开存。// store.ts import { create } from zustand; interface MonitorState { latest: Recordstring, SensorData; alerts: AlertItem[]; connection: online | offline | reconnecting; upsertData: (data: SensorData[]) void; setAlerts: (alerts: AlertItem[]) void; setConnection: (s: MonitorState[connection]) void; } export const useMonitorStore createMonitorState((set) ({ latest: {}, alerts: [], connection: offline, upsertData: (list) set((s) { const next { ...s.latest }; for (const item of list) next[${item.stationId}:${item.metric}] item; return { latest: next }; }), setAlerts: (alerts) set({ alerts }), setConnection: (connection) set({ connection }), }));用stationId:metric作为 key天然避免同站点多指标互相覆盖。组件侧通过选择器拿自己关心的那一条数据subscribe粒度细到键级别只有对应 key 变化才会触发重渲染。3.2 WebSocket 心跳与自动重连如果决定用长连接心跳重连逻辑要在 hook 里一次做对。核心参数是心跳间隔heartbeat 30s、重连退避指数退避 1s/2s/4s封顶 30s、断线状态写回 store。// useMonitorSocket.ts export function useMonitorSocket(url: string) { const setConnection useMonitorStore((s) s.setConnection); const upsertData useMonitorStore((s) s.upsertData); useEffect(() { let ws: WebSocket | null null; let timer: ReturnTypetypeof setInterval | null null; let retry 0; let closed false; const connect () { setConnection(reconnecting); ws new WebSocket(url); ws.onopen () { retry 0; setConnection(online); timer setInterval(() ws?.send(ping), 30000); }; ws.onmessage (ev) { const list parseBatch(ev.data); // 服务端批量推送 if (list.length) upsertData(list); }; ws.onclose () { if (timer) clearInterval(timer); if (closed) return; const delay Math.min(1000 * 2 ** retry, 30000); setTimeout(connect, delay); }; ws.onerror () ws?.close(); }; connect(); return () { closed true; ws?.close(); }; }, [url, upsertData, setConnection]); }这里把退避封顶放在 30 秒是因为环境监测的告警容忍度一般在分钟级超过 30 秒重连没有意义反而要主动提示运维介入。3.3 高频更新的渲染隔离WebSocket 每 5 秒推一批数据如果让 React 组件逐条 setState表格和图表都会跟着频繁重绘。常用做法是「数值走 store、动画走 CSS、图表走实例方法」折线图只把新的点appendData到 ECharts 实例上而不是把整个 option set 回去。这样图表重绘不经过 React reconcile帧率能稳很多。另外指标卡上的数值变化可以用 antd 的Statistic组件配valueStyle的显隐动画避免数字跳动像弹跳球一样干扰视觉。4. 可视化图表与大屏自适应4.1 图表选型ECharts 仍是环境监测的首选antd 生态里没有内置图表库可选的是 ECharts、Ant Design Charts基于 G2Plot和轻量自绘。环境监测的场景是「时间序列折线 散点 仪表盘」为主ECharts 的 dataset 机制和大数据量 mode 在这个领域最成熟。Ant Design Charts 胜在 API 简单但遇到多 y 轴、自定义 tooltip 时配置反而绕。因此更倾向用 echarts-for-react 封装一层留出透传 option 的口子。4.2 封装一个可复用的 TrendChart 组件// TrendChart.tsx import React, { useEffect, useRef } from react; import * as echarts from echarts; interface Props { series: { name: string; data: [number, number][] }[]; height?: number; } export default function TrendChart({ series, height 260 }: Props) { const ref useRefHTMLDivElement(null); const chartRef useRefecharts.ECharts | null(null); useEffect(() { const chart echarts.init(ref.current!); chartRef.current chart; const observer new ResizeObserver(() chart.resize()); observer.observe(ref.current!); return () { observer.disconnect(); chart.dispose(); }; }, []); useEffect(() { chartRef.current?.setOption( { tooltip: { trigger: axis }, xAxis: { type: time }, yAxis: { type: value, scale: true }, series: series.map((item) ({ name: item.name, type: line, showSymbol: false, areaStyle: item.name pm25 ? { opacity: 0.08 } : undefined, data: item.data, })), }, { notMerge: true } ); }, [series]); return div ref{ref} style{{ width: 100%, height }} /; }注意两点ResizeObserver监听容器尺寸变化而不是只监听 window resize因为 antd 的栅格布局折叠时图表容器会变窄notMerge: true保证 series 变化时旧配置被替换避免残留已经下线的测点曲线。封装好一层后卡片页、大屏页、历史查询页可以共用一个组件样式差异只靠 props 控制。4.3 大屏页的 vw/vh 适配与缩放监控大屏是环境监测网站的标配。大屏通常是 16:9 固定分辨率例如 1920×1080投到拼接屏直接在 factor 上写 px 最稳妥。常见适配方案有三类方案原理优点缺点vw/vh 布局字号、间距全部用视口单位实现简单无 JS 延迟非 16:9 屏会被拉伸变形rem 基准字号根据宽度动态调整 html font-size等比缩放文字图表 canvas 不跟随 remtransform: scale等比例缩放整个根容器内容不变形图表一并缩放容器外留白需居中处理大屏项目习惯用第三种设计稿 1920×1080 固定根容器按实际宽高算 scale 值页面内部继续用 px 开发。这样 ECharts 拿到的始终是设计稿尺寸不会出现 rem 方案下 canvas 图表与周边文字缩放不一致的违和感。// useScale.ts export function useScale(designWidth 1920, designHeight 1080) { const [scale, setScale] useState(1); useEffect(() { const apply () { const sx window.innerWidth / designWidth; const sy window.innerHeight / designHeight; setScale(Math.min(sx, sy)); }; apply(); window.addEventListener(resize, apply); return () window.removeEventListener(resize, apply); }, [designWidth, designHeight]); return scale; }配合在根容器上写transform: scale(scale)即可完成整个大屏等比缩放。注意transform-origin要设为左上角并让外层容器 flex 居中否则会出现整体偏移到右下角的观感。4.4 指标卡布局栅格 状态色指标卡用 antdRow/Col布局Col的flex属性根据指标数量决定宽度。每个卡片内部结构建议统一为「标题 / 数值 / 趋势箭头 / 更新时间」四段其中趋势箭头用ant-design/icons的ArrowUpOutlined与ArrowDownOutlined配合statistic字段渲染背景色由quality状态推导const colorMap { good: rgba(22,119,255,0.06), warn: rgba(250,173,20,0.12), alarm: rgba(255,77,79,0.16), };不要用「绿/黄/红」的直觉直接映射要跟客户确认过「超标」的阈值定义再上颜色否则验收时会为「黄色算不算告警」来回扯皮。5. 表格、查询与告警联动5.1 antd Table 动态合并列历史查询有个典型需求同一站点在某段时间内有多条监测因子记录表格里「站点名称」列需要跨行合并避免满屏重复的站点名。用 antd Table 实现动态合并的核心是两个函数onCell返回rowSpan以及一个预计算合并矩阵的辅助函数。// mergeCells.ts export function buildMergeMap(rows: Recordstring, unknown[], key: string) { const map new Mapstring, number(); rows.forEach((row) { const v String(row[key]); map.set(v, (map.get(v) ?? 0) 1); }); const done new Setstring(); return (row: Recordstring, unknown) { const v String(row[key]); if (done.has(v)) return { rowSpan: 0 }; done.add(v); return { rowSpan: map.get(v) }; }; }在列配置里这样用{ title: 站点, dataIndex: stationName, onCell: (row: Recordstring, unknown) ({ rowSpan: total 1 ? mergeMap(row)?.rowSpan : 1, }), }注意一个坑rowSpan: 0表示当前单元格被隐藏、由上方单元格纵向合并覆盖。如果同时还有跨页分页合并范围只在当前页数据内计算跨页合并需要把合并信息放在查询接口返回里前端不要自己猜。把pageSize调大50 行以内能显著减少合并边界 bug。5.2 查询表单配置化查询区如果按「温度、湿度、PM2.5、PM10、噪声」逐个写Form.Item每加一个指标都要改 JSX。环境监测指标后期几乎必扩可以考虑用配置项生成查询表单// queryConfig.ts export const metricOptions [ { label: 温度(℃), value: temp, input: input }, { label: 湿度(%RH), value: humidity, input: input }, { label: PM2.5(μg/m³), value: pm25, input: select, options: [优, 良, 超标] }, { label: 噪声(dB), value: noise, input: input }, ];页面里用map渲染Form.Item提交参数也由配置生成后端查询条件跟着metricOptions走。新增指标时只改这一份配置表不用动页面结构。参数上要注意时间选择器用 antdRangePicker时传给后端的是dayjs对象需要先.format(YYYY-MM-DD HH:mm:ss)再拼到查询串里否则拿到[object Object]查不到数据。5.3 告警联动一处状态三处响应环境监测最有价值的联动是「一处告警、三处响应」指标卡变红、表格对应行高亮、侧栏告警列表插入新记录。这三处的数据源都从alertsstore 取但展示逻辑不同。表格高亮用rowClassName判断当前站点是否有未确认告警侧栏用 antdBadge的count显示未读数指标卡背景色跟随quality字段变化。const rowClassName (record: HistoryRow) alertSet.has(record.stationId) ? row-alarm : ; // row-alarm 在 css 里写: background: #fff1f0注意不要把这三套展示逻辑堆在一个组件里否则告警一多整个页卡顿。拆成AlertList / AlertBadge / MetricCard三个小组件各自订阅 store不要把整个 store 传进去。6. 上线前必做的验证与防白屏手段6.1 白屏查因清单React 项目白屏最常踩的坑按优先级排序运行时未捕获异常导致整树卸载打包后静态资源 404入口文件里createRoot挂载的元素被其他组件提前替换。第二个问题在环境监测网站里很常见因为这类项目经常部署在内网带子路径的 Nginx 下vite.config.ts里没设base: ./资源全部 404。另外要养成习惯给根组件包一层 ErrorBoundaryclass AppErrorBoundary extends React.Component{ children: React.ReactNode } { state { hasError: false, message: }; static getDerivedStateFromError(error: Error) { return { hasError: true, message: error.message }; } render() { if (this.state.hasError) { return Result statuserror title页面异常 subTitle{this.state.message} /; } return this.props.children; } }这样至少不会出现整页白屏用户还能把错误信息报给你。错误边界不要包在路由内部而是包在ConfigProvider外面否则ConfigProvider自身异常同样会白屏。6.2 实测重渲染链路防住白屏后用 React DevTools Profiler 对三个典型交互做一次录制切换站点、收到一条实时推送、翻历史表格页。看火焰图里TrendChart、Table、MetricCard这几个组件有没有「未使用其 props 却重渲染」的节点。如果MetricCard明明只显示温度值却在一条 PM2.5 推送进来时也更新了那就是订阅粒度没控好回到第 3 章检查useMonitorStore的选择器写法。比较实用的自查方式是在组件里加一行console.count在 Profiler 之外也能确认重渲染次数。6.3 用 Chrome 性能面板验证图表帧率实时推送时折线图是否流畅用肉眼判断不可靠。在性能面板录制 10 秒关注两个指标Scripting耗时和Rendering耗时。如果Scripting持续大于 30%优先怀疑是 EChartssetOption传了整个大对象改为appendData后耗时通常能降到原来的三分之一。如果Rendering高检查是不是ResizeObserver在循环触发容器尺寸没变也反复调用chart.resize()。给resize加 200ms 的 debounce 即可。环境监测网站的上限基本由数据链路和渲染隔离决定把这两处收敛好后续加图表、扩指标都只是在稳定的骨架上添砖。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →