资讯详情

资讯详情

Python Flask+ECharts 轻量级数据可视化大屏实战

简介这份基于Python与Flask的轻量级数据可视化大屏项目面向需要快速搭建企业数据监控与业务分析界面的开发者及前端工程化学习者重点解决多页面看板切换、实时数据渲染与模块化展示需求。压缩包共24个文件约1.02MB包含Python后端脚本、HTML/JS/CSS页面与样式、PNG/GIF/JPG图片素材、字体、说明文档及许可证等目录中可见数据看板、空气质量监测、计算机性能指标等示例模块便于按结构阅读和二次改造。已有104人学习说明其在小体量实战项目中有一定参考价值。读者可据此理解Flask后端与前端页面的组织方式参考图表渲染、模块拆分和静态资源管理思路并结合附赠的前端工程化资料完成本地运行、界面定制与功能扩展。1. 需求先立住一套能现场演示的轻量大屏重点不在图表数量数据可视化大屏项目最常见的翻车点往往不是图表好不好看而是演示当天页面空白、数据不动、切页面卡顿。一套基于 Python 和 Flask 的轻量级大屏系统要解决的正是这类问题单进程后端提供 JSON 接口前端用原生 ES Module 组织页面ECharts 负责渲染通过页面注册表实现多页面切换用统一轮询器完成实时数据渲染。它适合两类人需要在一到两周内交出一套可运行大屏的课程设计或毕设以及要在内网机器上快速搭一块业务监控屏的开发者。系统内置数据看板、空气质量监测、计算机性能指标三个模块刚好覆盖指标卡、趋势图、分级配色、系统采集这四种典型渲染场景拿它当骨架改比自己从空文件夹起步省很多时间。2. 总体架构与技术选型Flask 单体 ECharts 的四层职责划分选型这一步最容易被带偏。看到数据大屏很多人的第一反应是 Vue3 Vite 组件库 微服务一整套搬上来结果两周时间有一周半花在配环境和解决依赖冲突上。这套方案反过来走后端一个 Flask 应用前端不引入构建工具用浏览器原生 ES Module 加载页面模块ECharts 作为唯一的第三方前端库手动放进 vendor 目录。代价是没法用 npm 生态收益是没有 node_modules、没有打包产物、没有 sourcemap 对不上改一行 JS 刷新就能看到效果。这个取舍成立的前提是页面数量可控。三个页面、十个以内的接口、没有复杂的前端状态共享原生 ESM 完全撑得住。如果后续指标涨到二十个页面、需要多角色权限和复杂表单联动那就该老老实实上框架。另一个硬约束是浏览器内网如果还有 IE 内核的老爷机这套方案直接不适用动手前先确认大屏那台机器跑的是什么。整体按四层划分职责数据适配层负责屏蔽数据来源差异本地 CSV、第三方接口、模拟数据服务层负责计算和缓存蓝图层负责定义 URL 契约前端负责渲染和调度。四层之间的边界定死后面换数据源、加页面都不会互相牵连。2.1 服务端三层蓝图、服务层、数据适配器Flask 项目写小的时候容易变成一个 app.py 里堆两百行的样子接口一多就没人敢改。这里按屏幕拆蓝图一块屏一个蓝图文件URL 前缀统一挂在/api下前端拿到路径就知道该找谁。应用工厂模式保留方便后面加测试配置和不同环境的数据源开关。# app.py from flask import Flask from blueprints.dashboard import bp as dashboard_bp from blueprints.air import bp as air_bp from blueprints.system import bp as system_bp def create_app(config_objectconfig.Config): app Flask(__name__) app.config.from_object(config_object) # 一块屏一个蓝图前端按前缀区分数据来源 app.register_blueprint(dashboard_bp, url_prefix/api/dashboard) app.register_blueprint(air_bp, url_prefix/api/air) app.register_blueprint(system_bp, url_prefix/api/system) # 所有接口统一禁缓存避免浏览器把实时数据当成静态资源存起来 app.after_request def no_store(resp): resp.headers[Cache-Control] no-store return resp return app if __name__ __main__: create_app().run(host0.0.0.0, port5000, threadedTrue)url_prefix是这套结构的关键参数前端采集器靠它拼请求threadedTrue让开发服务器能同时处理多个浏览器的请求否则一块屏的慢接口会拖住另一块屏。after_request里那条no-store不是可选项后面避坑章会专门讲不带它会出现什么现象。服务层和适配器是两件事别混在一起写。适配器只管把数据取回来并转成统一结构服务层只管算指标、算分级、存缓冲。这样做的直接好处是早期没有真实数据源写一个返回模拟值的适配器就能先把前端跑通等真实接口给了只改一个文件的取数逻辑。缓存用进程内的 TTL 字典不引 Redis。轻量场景下 Redis 是额外运维负担而 TTL 缓存能解决九成问题多个浏览器窗口同时打开同一块屏时后端只算一次。# services/cache.py import time class TTLCache: 极简 TTL 缓存几十行够用不引入外部依赖 def __init__(self, ttl3): self.ttl ttl # 秒需小于前端轮询间隔 self._store {} def get_or_set(self, key, producer): now time.time() hit self._store.get(key) if hit and now - hit[0] self.ttl: return hit[1] value producer() self._store[key] (now, value) return valuettl3对应前端 5 秒轮询中间留 2 秒余量既能合并并发请求又不会让数据明显滞后。如果某项指标采集很贵比如要调外部接口可以单独给它一个更长的 TTL反过来性能指标这种必须新鲜的TTL 设成 1 秒即可。参数调错的表现很典型TTL 大于轮询间隔时前端会看到两个周期数据一模一样演示时会被追问是不是卡住了。2.2 前端三层外壳、页面、图表组件前端只分三层再多就过度设计了。外壳层是一个 HTML负责铺满屏幕、放缩放容器、挂载页面根节点和顶部导航页面层每个模块导出一个固定接口的对象承担这块屏有哪些图表、拉哪些接口、数据回来怎么塞图表组件层封装 ECharts 的初始化、尺寸变化和销毁页面层不直接碰echarts.init。页面模块的接口约定是整套结构的地基定死之后加页面就是填空题// static/js/pages/air.js import { createChart, resizeChart, disposeChart } from ../core/chart.js; export default { id: air, title: 空气质量监测, endpoints: [/api/air/realtime, /api/air/history?hours24], // 首次进入时调用一次建 DOM、init 图表 mount(root, ctx) { this.chart createChart(root.querySelector(#air-trend), line); }, // 每次轮询回来调用dataMap 的 key 就是 endpoints 里的路径 update(dataMap) { const realtime dataMap[/api/air/realtime]; const history dataMap[/api/air/history?hours24]; this.chart.setOption(buildOption(realtime, history)); }, // 离开页面时调用必须把实例和定时器清干净 unmount() { disposeChart(this.chart); this.chart null; } };约定三件事endpoints声明式地告诉采集器要什么数据页面自己不发请求update只做渲染不做出错处理兜底交给采集器统一做unmount必须释放图表实例不释放就是内存泄漏。buildOption这类纯函数放在页面文件内部方便单独调试。这一层其实就是一次前端工程化实战模块边界、依赖方向、生命周期钩子该有的都有只是没有构建工具参与。对课程设计来说答辩时把为什么不用框架、模块之间靠什么解耦讲清楚比堆一堆依赖更能体现对工程的理解。2.3 目录结构与依赖清单目录按后端能一眼找到接口、前端能一眼找到页面来排不做多层嵌套dashscreen/ ├── app.py # 应用工厂 ├── config.py # 数据源类型、TTL、端口等配置 ├── requirements.txt ├── blueprints/ # 每块屏一组接口 │ ├── dashboard.py │ ├── air.py │ └── system.py ├── services/ # 计算与缓存不碰 HTTP │ ├── cache.py │ ├── air_service.py │ └── metrics_service.py ├── adapters/ # 数据来源可整体替换为模拟实现 │ ├── air_source.py │ └── metrics_source.py ├── static/ │ ├── js/ │ │ ├── core/ # 路由、采集器、缩放、图表封装 │ │ └── pages/ # 每块屏一个模块 │ └── vendor/echarts.min.js └── templates/ ├── base.html └── pages/后端依赖只有两个前端依赖只有一个这个组合的安装成功率接近百分之百# requirements.txt Flask3.0 psutil5.9 # 仅计算机性能指标模块需要其他模块可去掉psutil是跨平台的Windows、Linux、macOS 都能直接用不需要为采集 CPU 和内存写平台分支。ECharts 用手动放入vendor的方式版本选 5.x原因是 5.x 的setOption增量更新行为稳定配合 5 秒一次的轮询不会出现全量重绘闪烁。版本号不需要锁死到小版本内网环境下载一次放进去就不再动了。3. 多页面切换与大屏外壳页面注册表、等比缩放与实例生命周期多页面切换在别的项目里可能只是路由的事在大屏项目里它有三个必须一起解决的问题切回来时图表不能空白、切走时定时器和实例必须回收、切换动画不能把图表容器高度压成 0。这三个问题不解决演示时切个五六次就会看到某块屏变白而这类问题在本地调试时往往复现不出来。方案是把页面信息收进一个数组路由、导航、预加载都从这一个数组读。新增一块屏只改这个数组和一个页面文件不需要动路由配置、菜单 HTML 和采集器的注册逻辑。3.1 页面注册表新增一块屏只改一个数组用 hash 路由而不是 history 路由原因很实际history 路由需要服务端把所有非 API 路径重写到index.html多一层 Nginx 配置就多一个出错的点而现场演示时刷新页面报 404 是最尴尬的情况之一。hash 路由不需要服务端配合直接双击 HTML 也能跑。// static/js/core/routes.js export const PAGES [ { id: dashboard, path: #/dashboard, title: 数据看板, loader: () import(../pages/dashboard.js), // 按需加载首屏更快 keepAlive: true // 回到该页时不重新 init只刷新数据 }, { id: air, path: #/air, title: 空气质量监测, loader: () import(../pages/air.js), keepAlive: false }, { id: system, path: #/system, title: 计算机性能指标, loader: () import(../pages/system.js), keepAlive: false } ]; export const DEFAULT_PAGE dashboard;loader用动态import()页面模块首次进入才下载三个页面的 JS 不会挤在首屏一起加载。keepAlive是这块屏是否需要保活数据看板作为默认页来回切时保留 DOM 和图表实例切回来只更新数据视觉上更连贯另外两块屏每次重新挂载内存占用可控。DEFAULT_PAGE单独导出是为了在地址栏没有 hash 时能落到正确的首屏避免打开就是白屏。切换动作本身要做两件事先unmount旧页面再等一帧让容器的display恢复成block然后mount新页面。少等这一帧ECharts 初始化时读到的容器宽高就是 0图表会缩成一条线这是新手最常踩的一个坑后面还会展开讲。3.2 1920×1080 设计稿等比缩放与全屏适配大屏交付时永远无法确定现场屏幕的分辨率。合理做法是按 1920×1080 出设计稿运行时整体等比缩放并居中多出来的区域留黑边。这样字号、间距、图表位置都按设计稿写死不需要为每个分辨率写一套媒体查询。// static/js/core/scale.js const DESIGN_W 1920; const DESIGN_H 1080; export function fitScreen(el) { const scale Math.min(window.innerWidth / DESIGN_W, window.innerHeight / DESIGN_H); const offsetX (window.innerWidth - DESIGN_W * scale) / 2; const offsetY (window.innerHeight - DESIGN_H * scale) / 2; el.style.position absolute; el.style.width ${DESIGN_W}px; el.style.height ${DESIGN_H}px; el.style.transformOrigin left top; el.style.transform scale(${scale}); el.style.left ${offsetX}px; el.style.top ${offsetY}px; return scale; }Math.min保证宽高比不变不会出现横向拉伸transformOrigin: left top配合手动算的offsetX/offsetY实现居中。缩放比例一般落在 0.7 到 1.5 之间超出这个范围说明设计稿尺寸和现场屏幕差距太大应该改用 1600×900 的稿子重新出一版而不是硬缩。窗口尺寸变化要节流resize事件触发频率很高不节流会导致缩放函数一秒钟跑几十次画面抖动。用 100ms 的防抖足够同时每次缩放完成后统一通知图表层重新计算尺寸。需要注意整体transform缩放会让 canvas 的位图被拉伸缩放比例不是 1 的时候图表文字会有轻微发虚。这个问题的处理方式在避坑章里详细讲因为它是这套方案里唯一需要权衡的取舍。3.3 ECharts 实例的复用、销毁与 resize 时机图表封装的三个动作建、改、销。建的时候记下实例和容器改的时候只传setOption不重新 init销的时候dispose并把引用置空。实例统一存在一个 Map 里页面卸载时按容器查出来销毁避免某个页面忘了写销毁代码。// static/js/core/chart.js import * as echarts from ../vendor/echarts.min.js; const registry new Map(); // container - chartInstance export function createChart(container, type) { if (!container) throw new Error(图表容器不存在检查模板 id); if (registry.has(container)) return registry.get(container); const chart echarts.init(container, null, { renderer: canvas, // 缩放场景下提高清晰度避免文字发虚 devicePixelRatio: Math.max(2, window.devicePixelRatio || 1) }); chart.__type type; registry.set(container, chart); return chart; } export function resizeAll() { // 只在外部统一下发 resize页面自己不要监听 window.resize registry.forEach((chart) chart.resize()); } export function disposeChart(chart) { if (!chart) return; registry.forEach((v, k) { if (v chart) registry.delete(k); }); chart.dispose(); }renderer: canvas是默认值写出来是为了明确不支持用 SVG 渲染器的扩展图表。devicePixelRatio提高到至少 2 是解决模糊的关键参数代价是内存占用上升在普通办公机上跑三块屏没问题如果指标很多要降回 1.5。resizeAll集中在缩放模块里调用页面模块一律不自己监听window.resize否则同一个窗口变化会被触发多次曲线会闪。还有一个细节setOption的第二个参数要传{ notMerge: false }也就是默认的合并模式。合并模式下长度相同的数组会平滑过渡看起来像实时滚动如果用notMerge: true每次都是全量替换动画会从头开始视觉上很跳。4. 三个数据面板与实时链路数据契约、采集调度与降级三个模块对应三种不同的数据形态正好把大屏里最典型的渲染场景覆盖全了。数据看板是固定结构 趋势空气质量是分级 配色性能指标是高频采样 滚动窗口。把它们拆开写不是内容不够凑数而是因为三者的数据契约设计思路完全不同混在一起讲反而说不清。实时链路的公共部分抽成统一采集器页面只声明要哪些接口采集器负责什么时候拉、失败了怎么办、页面不可见时停不停。4.1 数据看板指标卡与趋势图的数据契约看板是打开系统第一眼看到的东西结构要稳定。上面一排指标卡下面是趋势折线接口设计成一次请求把所有东西带回来避免首屏要发四五个请求。// GET /api/dashboard/summary { updatedAt: 2024-05-20T10:32:15, kpi: [ { key: orders, label: 今日订单, value: 1284, unit: 单, trend: 0.12 }, { key: revenue, label: 今日营收, value: 86210, unit: 元, trend: -0.03 }, { key: online, label: 在线设备, value: 47, unit: 台, trend: 0.00 } ], trend: { x: [00:00, 02:00, 04:00, 06:00], series: [{ name: 订单量, data: [12, 8, 5, 21] }] } }trend字段用小数表示环比前端决定显示成红色还是绿色、箭头朝哪边这样配色规则留在前端后端只给数值换皮肤不用改代码。updatedAt一定要带前端拿它渲染最后更新时间演示时被问到数据是不是旧的时候这个时间戳就是证据。指标卡的数字不要直接赋值用一个小函数做滚动动画300ms 内从旧值过渡到新值。看起来是锦上添花实际作用是把数据在动这件事可视化出来观众对静止数字的感知很弱对跳动的数字更敏感。4.2 空气质量监测AQI 分级、颜色映射与历史曲线AQI 的分级和配色是最容易被写重的地方。常见错误是后端算一遍等级、前端再算一遍颜色两边阈值不一致出现显示中度污染但底色是黄色的尴尬。正确做法是分级逻辑只放在后端接口直接返回等级文案和颜色值前端只负责用。# services/air_service.py # AQI 六级划分阈值与配色写在一处前端不再重复判断 AQI_LEVELS [ (50, 优, #00e400), (100, 良, #ffff00), (150, 轻度污染, #ff7e00), (200, 中度污染, #ff0000), (300, 重度污染, #99004c), (10**9, 严重污染, #7e0023), ] def classify(aqi: float) - dict: 返回等级文案与颜色前端直接取用 for limit, label, color in AQI_LEVELS: if aqi limit: return {level: label, color: color} return {level: 未知, color: #999999} def build_realtime(raw: dict) - dict: aqi float(raw.get(aqi, 0)) meta classify(aqi) return { aqi: aqi, level: meta[level], color: meta[color], # 六个分项用于雷达图 items: [ {name: PM2.5, value: raw.get(pm25, 0)}, {name: PM10, value: raw.get(pm10, 0)}, {name: SO2, value: raw.get(so2, 0)}, {name: NO2, value: raw.get(no2, 0)}, {name: CO, value: raw.get(co, 0)}, {name: O3, value: raw.get(o3, 0)}, ], }10**9这一档是兜底保证任何异常大的数值都能落到某个等级不会返回None让前端崩掉。配色用的是 AQI 通用色阶从绿到褐红颜色本身携带信息观众不需要看数字就能判断好坏。前端拿到color之后用它同时设置数值文字颜色、等级标签背景和趋势线的主色一处数据三处使用。历史曲线用 24 小时逐小时数据接口单独一个/api/air/history?hours24和实时接口分开。分开的原因是两者的刷新频率差一个数量级实时数据 5 秒一次历史数据 5 分钟一次就够。如果塞进同一个接口历史数据会被反复重复传输白白浪费带宽。4.3 计算机性能指标psutil 采集与环形缓冲性能指标这块是实时二字最实在的体现CPU 和内存每秒都在变前端要看到的是最近几分钟的趋势不是某一瞬间的快照。所以后端不能只存一个当前值要维护一段定长历史。# services/metrics_service.py import time from collections import deque import psutil class MetricsBuffer: 定长环形缓冲只保留最近 N 个采样点内存占用恒定 def __init__(self, size60): self.cpu deque(maxlensize) self.mem deque(maxlensize) self.size size # 第一次调用 cpu_percent 没有基线先预热一次 psutil.cpu_percent(intervalNone) def sample(self) - dict: self.cpu.append(psutil.cpu_percent(intervalNone)) self.mem.append(psutil.virtual_memory().percent) disk psutil.disk_usage(/) return { cpu: list(self.cpu), mem: list(self.mem), disk: {used: disk.used, total: disk.total, percent: disk.percent}, ts: int(time.time() * 1000) }deque(maxlen60)是关键参数超过 60 个点自动丢弃最老的不需要手动裁剪也不会有内存增长。60 个点 × 5 秒 5 分钟窗口正好是一块屏能看清趋势又不至于太挤的长度。intervalNone表示非阻塞采样返回的是距上次调用之间的平均值所以必须保证采样是周期性触发的不能想起来才调一次否则数值会失真。构造函数里那次预热不能省。psutil.cpu_percent()第一次调用没有历史基线固定返回 0.0如果不在启动时预热前端第一条曲线开头永远是 0看起来像一次凭空出现的掉底。disk_usage(/)在 Windows 上也能正常工作会自动映射到系统盘。采集不要放在请求处理函数里而是由后端一个后台线程按固定间隔调用sample()接口只读缓冲。这样即使前端十秒没来请求曲线也是连续的不会出现断层。4.4 统一采集器轮询节流、缓存与断流兜底前端最容易失控的地方就是定时器。每个页面自己setInterval切一次页面多一个切十次就是十个定时器同时打接口页面越用越卡接口压力翻倍。统一采集器只有一个定时器所有页面注册进来页面不可见时整体暂停。// static/js/core/poller.js export class Poller { constructor({ interval 5000, timeout 4000 } {}) { this.interval interval; // 轮询间隔毫秒 this.timeout timeout; // 单次请求超时必须小于轮询间隔 this.tasks new Map(); // key: url, value: { pageId, latest } this.timer null; this.failCount 0; } register(pageId, urls) { urls.forEach((url) this.tasks.set(url, { pageId, latest: null })); } unregister(pageId) { [...this.tasks.entries()] .filter(([, v]) v.pageId pageId) .forEach(([k]) this.tasks.delete(k)); } start() { if (this.timer) return; this.tick(); this.timer setInterval(() this.tick(), this.interval); } stop() { clearInterval(this.timer); this.timer null; } async tick() { const entries [...this.tasks.entries()]; await Promise.all(entries.map(async ([url, task]) { const ctrl new AbortController(); const t setTimeout(() ctrl.abort(), this.timeout); // 超时即放弃避免请求堆积 try { const resp await fetch(url, { signal: ctrl.signal, cache: no-store }); task.latest await resp.json(); this.failCount 0; } catch (e) { this.failCount 1; // 失败计数用于触发降级提示 task.latest task.latest || { error: true }; } finally { clearTimeout(t); } })); this.emit(); } emit() { // 按页面分组回调页面只收到自己的数据 const byPage {}; this.tasks.forEach((v, url) { byPage[v.pageId] byPage[v.pageId] || {}; byPage[v.pageId][url] v.latest; }); this.onData this.onData(byPage); } }timeout必须小于interval否则上一轮请求还没结束下一轮就开始了请求会越堆越多最后浏览器同时挂着几十个请求表现为页面卡而不报错。cache: no-store和前面的后端响应头是双保险。failCount是给降级用的连续失败超过 3 次就在屏幕上打一条数据延迟的提示条而不是让图表停在旧数据上假装正常。页面切换时调unregister清掉旧页面的接口register注册新页面的采集器内部只维护一份任务表不存在定时器泄漏。监听document.visibilitychange页面切到后台就stop()切回来start()浏览器标签页长时间挂后台时能省下大量无效请求。5. 避坑与排查上线前最容易翻车的五个点下面这五条不是从教程里抄的而是这类项目里出现频率最高、又最难在本地复现的问题。每一条都按看到什么现象 → 为什么 → 怎么改写出问题时可以直接对着排查。5.1 切回页面后图表空白容器宽高为 0 的初始化现象很典型第一次打开正常切到第二块屏再切回来图表区域一片空白控制台没有任何报错resize一下窗口又能显示出来。原因在于 ECharts 初始化时会读取容器的clientWidth和clientHeight作为画布尺寸。如果初始化发生时容器还处于display: none或者动画的中间态读到的是 0画布就按 0 尺寸创建了。之后容器虽然显示出来但实例不会自动重新测量。解决办法是把显示容器和初始化图表拆成两步中间隔一帧// 切换页面时先让容器可见下一帧再初始化 container.style.display block; requestAnimationFrame(() page.mount(container));同时给createChart加一道断言容器宽高为 0 时直接抛错让问题在开发阶段暴露而不是留到演示现场。我在createChart里写的那句throw new Error(图表容器不存在)就是干这个的宽高检查同理。保活的页面keepAlive: true不销毁实例也就不会触发这个坑这也是为什么默认页建议设成保活。5.2 曲线来回跳变多进程下的内存态数据不一致现象是 CPU 曲线出现莫名其妙的锯齿这一秒 12%下一秒 78%再下一秒又回到 15%看起来像机器在疯狂抖动实际用任务管理器看很平稳。原因是生产环境用多 worker 启动后每个进程各自持有一份MetricsBuffer。前端第一个请求打到 worker A第二个打到 worker B两份缓冲的历史不一样拼起来就是锯齿。这个问题在开发环境永远复现不了因为flask run是单进程。三条路按场景选演示和课程设计场景直接用单 worker 多线程启动gunicorn -w 1 --threads 8 app:create_app()最省事需要多 worker 又要曲线连续就把缓冲下沉到共享存储用文件或 Redis 存最近 60 个点第三种是把采样和展示分离由一个独立的采集进程定时写存储Web 进程只读。轻量场景下第一条足够但心里要清楚这是个已知限制答辩被问到时能答上来比假装没有强。5.3 越切越卡定时器没人回收现象是刚打开很流畅来回切十几分钟后切换明显变慢打开开发者工具的网络面板会发现同一时刻有十几条一模一样的请求在飞。根因有两个往往同时出现页面模块里自己写了setIntervalunmount时忘了clearInterval或者图表实例没有dispose每次切回来都新建一个旧实例还挂在内存里窗口尺寸变化时所有实例一起resize越跑越慢。统一采集器就是为了掐死第一个原因页面一律不写定时器。第二个原因靠unmount里强制disposeChart解决并且在开发阶段加一条简单的自检切换二十次之后在控制台打印registry.size如果数字一直增长说明有实例没释放。5.4 数据冻住被缓存吃掉的实时请求现象是页面上所有数字都定住了刷新浏览器能恢复过一会儿又不动。看网络面板请求返回的是 200但响应头里带from disk cache或者from memory cache。GET 请求在浏览器和中间的代理层都可能被缓存尤其是接口路径固定、参数不变的情况下浏览器会认为资源没变直接返回本地副本。加时间戳参数是一种解法但那只是绕过不是根治。根治办法是后端在响应头里明确写Cache-Control: no-store前端fetch时也带上cache: no-store两头都堵住。如果中间还有一层 Nginx检查它有没有对/api开proxy_cache。判断是否彻底解决的方法很简单连续观察 30 秒updatedAt时间戳必须一直在变。5.5 缩放后图表发虚位图被拉伸的固有代价现象是把浏览器窗口调小或者换到 1366×768 的投影仪上图表文字变模糊鼠标悬停的热区也和视觉位置对不上差个几像素。原因是整体transform: scale()只是视觉变换canvas 内部的位图还是按 1920×1080 渲染的缩放比例不是 1 的时候位图被重采样文字边缘就糊了。触控和鼠标坐标也要经过逆变换处理不好就会偏移。缓解办法有三层初始化时把devicePixelRatio提到 2 以上让原始位图分辨率更高缩放后损失更小图表内的字号不要贴着 10px 这种极限值写设计稿里最小字号定在 14px投影前尽量让实际分辨率接近 1920×1080缩放比例控制在 0.85 到 1.2 之间。如果现场屏幕是 4K宁可重新出一版 2560×1440 的设计稿也不要硬把 1080 的稿子放大一倍多。这一点没有完美解只能控制损失范围交付时提前跟使用方说清最佳分辨率比现场解释强。6. 验收与答辩用一张自查表把演示风险压到最低功能做完不等于能交付。大屏这类东西的验收标准不在功能列表全部点亮而在连续跑两小时、被十几个人来回切、网络抖一下还能正常显示。我一般在交付前留出半天专门按下面这张表逐条过一遍发现问题就地改不改完不交付。检查项判断标准不合格时怎么处理连续运行挂机 2 小时内存占用增幅不超过 20%查图表实例是否泄漏打印 registry.size 对比快速切换三块屏来回切 20 次无空白、无卡顿检查 mount 时机与 unmount 是否释放干净断网恢复断开网络 30 秒再恢复30 秒内自动恢复更新检查 failCount 降级提示与轮询是否仍在跑分辨率适配1366×768 与 2560×1440 下都不溢出、不裁切调整缩放区间必要时重出设计稿尺寸直接进子页地址栏直接输入#/system刷新正常显示检查 hash 解析与默认页回退逻辑后端重启重启服务后前端不刷新页面也能恢复检查失败重试是否停止必要时加手动重连时间戳连续观察 30 秒updatedAt 一直在变检查缓存头、浏览器缓存、代理缓存三层演示机预热在演示用的那台机器上完整跑一遍现场机器性能和字体都可能不同必须实机验证这张表里最容易被跳过、又最容易出事的是最后一条。开发机是新机器、大屏机器可能是会议室里那台装了不少东西的老电脑字体缺失导致数字宽度变化、显卡驱动导致 canvas 渲染异常这些都只有实机才能暴露。条件允许的话提前一天把整套东西拷到演示机上跑一遍比临场救火划算得多。答辩环节有一个技巧特别管用把实时这个词拆成三个可调参数讲——数据源采集频率、后端缓存 TTL、前端轮询间隔。评审问你这算不算实时的时候不要笼统回答是或不是而是说清楚采集 5 秒一次、缓存 3 秒、轮询 5 秒端到端延迟在 10 秒以内如果需要更快把这三个数一起往下调代价是 CPU 采集精度下降和接口压力上升。这样回答既表明了实现细节也表明了你知道边界在哪比背功能列表有效得多。同样值得准备的是取舍类问题为什么不用 Vue、为什么不用 WebSocket、为什么分级逻辑放在后端。这几个问题的答案在前面章节里都有提前组织成两三句话答的时候直接说结论加一句理由不要现场推导。从那以后我每次做大屏类项目交付前都会把这张表打印出来一条一条打勾勾完才敢约演示时间。有几次就是靠实机预热和连续运行这两条提前发现了在开发机上完全看不出来的问题。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →