简介:一套基于Python与Flask构建的轻量级数据可视化大屏展示系统,适合前端工程师、Python开发者及企业数据监控人员使用。系统支持多页面切换与实时数据渲染,内置数据看板、空气质量监测、计算机性能指标等模块,可快速搭建企业级业务分析大屏,降低可视化项目从零开发的门槛。资源包共24个文件、约1.02MB,包含4个Python后端脚本、3个JavaScript交互文件、2个HTML页面及CSS样式,辅以多张图片、字体、说明文档和项目说明等,目录结构清晰,便于按模块检索与二次开发。目前已有104人学习,适合希望掌握Flask和前端数据可视化完整流程的读者。借助这份工程化实战资源,可深入理解前后端分离架构、WebSocket实时推送以及大屏项目的模块组织方式,并以此为基础扩展到更多监控场景。
1. 数据可视化大屏不只有重型前端这一条路:Flask 方案的落地价值
第一次接到“做一块企业数据可视化大屏”的需求时,我差点被各种动辄几百 MB 的框架劝退。后来发现,一块真正每天开着、要投屏到会议室的企业大屏,核心诉求压根不是炫酷,而是稳定、好改、能接数据——而基于 Python 和 Flask 的这套轻量级方案,正好踩在这个点上。这个标题指的并不是某个自成体系的大屏框架,而是一条工程路径:后端用 Flask 出数据接口,前端用 ECharts 渲染图表,页面之间通过蓝图路由切换,把数据看板、空气质量监测、计算机性能指标这类模块组织在一个工程里。适合谁?适合团队里已经是 Python 技术栈、需要快速交付展示型页面、不想为一个大屏引入 Node 服务和复杂前端的场景。主张也直接:轻量不等于简陋,把数据和渲染的边界处理好,Flask 就能撑起像样的企业数据监控和业务分析大屏。
2. 拆解 Flask 大屏项目结构:为什么这样选型,以及最小可运行骨架
2.1 选择 Flask 而非 Django 或 Node:先看大屏的本质工作
大屏开发的技术选型,最容易被“哪个框架火”带偏。我见过用 Django 搭大屏的,路由、ORM、Admin 全上,数据就三十条,光初始化就压得人喘不过气;也见过用 Node 写服务端只为配个 ECharts,前后端两套工程,部署时还得同时维护两个进程。大屏这个场景的本质工作是三件事:提供数据接口、渲染模板页面、按模块组织路由。这三件事 Flask 都只用一个文件加几个装饰器就能解决。
| 方案 | 启动成本 | 数据处理链路 | 与 Python 数据生态的距离 | 适合场景 |
|---|---|---|---|---|
| Flask | 低,单进程可跑 | 可直接引用 pandas、psutil、数据库驱动 | 最近,数据采集与接口在同一进程 | 企业内部大屏、快速原型、中小规模监控 |
| Django | 高,自带 ORM/Admin/迁移 | 近,但样板代码多 | 近,但重 | 需要完整后台管理、用户权限的大系统 |
| Node/Express | 中,依赖 npm 生态 | 远,需要另写 Python 服务配合 | 远,跨语言沟通成本高 | 团队已是 TS 全栈、前后端同构的团队 |
我的判断标准很简单:如果数据获取、清洗、计算这部分已经或将要由 Python 承担,那么 Flask 是让前后端共用一套语言和进程的最短路径。大屏页面本身可以写成一个模板,ECharts 通过静态文件引入,接口直接用jsonify输出,前后端不需要分离到两个仓库。这个选择在企业内网环境里还有个隐性优势:部署时只需要一个 Python 进程,不需要额外维护 Node 运行时。
2.2 目录设计:把蓝图、模板、静态资源分清楚
拿到一套大屏源码,第一步不是看图表配置,而是看目录是否按“页面模块”和“静态资源”做了分离。我一般会用按模块拆 Blueprint 的结构,而不是把所有路由堆在 app.py 里。原因很实际:数据看板、空气质量监测、计算机性能指标各有一个 Blueprint,后续加模块不会互相污染,也不会出现一个坏接口拖垮整页的情况。
project_root/ ├── app.py # Flask 应用入口,注册蓝图 ├── blueprints/ │ ├── dashboard.py # 数据看板模块 │ ├── air.py # 空气质量监测模块 │ └── performance.py # 计算机性能指标模块 ├── templates/ │ ├── base.html # 公共导航与布局 │ ├── dashboard.html │ ├── air.html │ └── performance.html ├── static/ │ ├── css/ │ │ └── screen.css # 大屏缩放与样式 │ ├── js/ │ │ ├── echarts.min.js # 本地化的 ECharts │ │ └── charts/ # 各页面图表初始化脚本 │ └── data/ │ └── mock_live.py # 模拟数据源,便于替换 └── requirements.txt这个目录对应了一个很关键的设计原则:数据源是可替换的。mock_live.py专门负责生成测试数据,真实环境里把里面的函数改成读数据库或调用上游接口,图表代码完全不用动。ECharts 放在static/js/echarts.min.js而不是引用 CDN,也是做企业内网大屏的常识——很多会议室网络根本访问不了外网,现场去下载依赖是最尴尬的翻车现场。
2.3 第一次跑通:Flask 路由加模板渲染的最小命令
不用等整套代码齐全,先把这个最小流程跑通再谈图表。创建一个最简单的 Flask 应用,让它能渲染一个包含 ECharts 的页面:
# app.py from flask import Flask, render_template app = Flask(__name__) @app.route("/") def dashboard(): return render_template("dashboard.html") if __name__ == "__main__": # 0.0.0.0 允许局域网其他设备访问,参考实际部署环境按需设置 app.run(host="0.0.0.0", port=5050, debug=True, threaded=True)模板部分先不写具体图表,只确认静态资源加载正常:
<!-- templates/dashboard.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>数据可视化大屏</title> <!-- Flask 静态文件引用,url_for 会自动拼接 static 路径 --> <script src="{{ url_for('static', filename='js/echarts.min.js') }}"></script> </head> <body> <div id="main" style="width:600px;height:400px;"></div> <script> var chart = echarts.init(document.getElementById('main')); chart.setOption({ title: { text: '最小可运行示例' }, // 中文标题,用于验证编码 xAxis: { type: 'category', data: ['A', 'B'] }, yAxis: { type: 'value' }, series: [{ type: 'bar', data: [10, 20] }] }); </script> </body> </html>启动后访问http://localhost:5050,能看到柱状图就说明这一套底座通了。host="0.0.0.0"是让同一局域网里的其他电脑可以通过服务器 IP 访问,方便在企业内网投屏演示;debug=True只建议开发期用,它能热重载代码,但也会泄露调试信息并拖慢并发能力。threaded=True是让开发服务器能处理并发请求的开关,但注意它只是“能用”,不是“扛得住”,后面第 5 章会专门讲并发的坑。
3. 实时数据渲染:轮询机制、ECharts 更新与刷新参数设多少
3.1 轮询不是偷懒:轻量级场景里 WebSocket 的性价比
“实时渲染”这四个字,最容易让新人直接冲去上 WebSocket。实际上在企业数据监控大屏里,绝大多数指标的实时性要求是秒级甚至分钟级,比如 CPU 使用率、空气 PM2.5、业务订单量,这些数据用 HTTP 轮询完全覆盖得住。
| 方案 | 实现成本 | 断线处理 | 服务端压力 | 适用数据节奏 |
|---|---|---|---|---|
| HTTP 轮询 | 低,原生 fetch 即可 | 低,无状态重连 | 每 N 秒一个请求,可控 | 每秒到每分钟变化 |
| SSE | 中,EventSource 单向推送 | 中,浏览器自动重连 | 长连接占用,压力小 | 秒级变化、单向展示 |
| WebSocket | 高,需引入 Flask-SocketIO | 高,需自己处理重连/心跳 | 长连接,适合双向交互 | 毫秒级、双向操作 |
我做这类轻量大屏,默认先上轮询,理由很朴素:大屏是“看得多、点的少”,基本没有客户端向服务端推送的需求。WebSocket 引入的心跳、重连、连接状态管理,在只有二十个指标点位的场景里完全是负担。如果某一天数据源的实时性要求真的到了毫秒级,再去加 Flask-SocketIO 也来得及,路由和数据接口不用推翻。
3.2 数据接口与 setOption:让图表按秒跳动的最小实现
实时渲染的核心链路只有三步:后端准备最新数据、前端定时请求、把新数据塞进 ECharts 的setOption。先写后端接口:
# blueprints/dashboard.py import time import random from flask import Blueprint, jsonify dashboard_bp = Blueprint("dashboard", __name__) def generate_live_data(): """模拟业务数据,真实环境替换为数据库或上游接口查询""" now = time.strftime("%H:%M:%S") return { "time": now, "order_count": random.randint(50, 150), # 模拟当前订单量 "revenue": round(random.uniform(2000, 8000), 2) # 模拟金额,保留两位小数 } @dashboard_bp.route("/api/live_data") def live_data(): data = generate_live_data() return jsonify(data)这段代码的重点不在随机数,而在接口的返回结构。time字段供 ECharts 的 x 轴做时间标签,order_count和revenue是两个独立的数值维度,前端拿到后可以分别塞进柱状图和折线图。random.uniform得到的浮点数如果不做round处理,列表里会出现一长串小数,影响大屏展示的可读性,这是很容易漏掉的小细节。
前端用一个轮询函数驱动更新:
// static/js/dashboard_live.js var chart = echarts.init(document.getElementById('chart_main')); function fetchLiveData() { fetch('/api/live_data') // 默认 GET 请求 .then(function(resp) { return resp.json(); }) .then(function(data) { // notMerge 告诉 ECharts 丢弃旧数据重新合并,避免残留上一次的序列 chart.setOption({ xAxis: { data: [data.time] }, // 只保留最新一个时间点 series: [{ name: '订单量', data: [data.order_count] }] }, true); }) .catch(function(err) { console.error('实时数据请求失败', err); }); } // 每 3 秒拉一次,间隔可根据数据变化频率调整 setInterval(fetchLiveData, 3000); // 大屏窗口尺寸变化时重绘图表,否则图表会被拉伸变形 window.addEventListener('resize', function() { chart.resize(); });这里两个参数需要说明。setOption的第二个参数true对应notMerge,当你用“只保留一个时间点”的方式覆盖数据时,必须让 ECharts 丢掉上一次的旧数据,否则新旧序列叠加会出现忽多忽少的情况。setInterval的间隔 3000 毫秒是个经验值:比人的视觉刷新频率高一个量级,同时又不会给后端造成明显压力。如果你需要让时间轴滚动展示最近 N 个点,就要在前端维护一个数组,每次 push 新数据后slice(-N)截断,再整体传给xAxis.data和series.data。
3.3 刷新频率与请求合并:三个参数的权衡
轮询间隔不是拍脑袋定的,它由数据变化速度、后端处理耗时、浏览器并发连接数共同决定。
| 参数 | 建议值 | 说明 |
|---|---|---|
| poll_interval | 3000–5000ms | 企业内部监控指标的变化以分钟级为主,太快无意义 |
| 单次接口返回数据条数 | 不超过 200 条 | 大屏图表只展示趋势,不用把全量历史塞进前端 |
| 同一页面并发轮询接口数 | 尽量合并为 1 个 | 多图表共用一套数据,避免请求堆积 |
最常见的问题是“三张图三个请求”,每个图表组件各自写一个setInterval,同时向后端发三个接口。这在大屏场景下是浪费:三个图表的数据往往来自同一个统计口径,合并到一个接口里返回,前端一次请求更新三个图表,既减少了服务端压力,也让图表之间的时间轴天然对齐。合并接口的结构一般是这样:
@dashboard_bp.route("/api/overview") def overview(): return jsonify({ "order": generate_order_data(), # 订单趋势 "revenue": generate_revenue_data(), # 收入趋势 "category": generate_category_data() # 品类占比 })另外,如果某个模块的数据本身是秒级变化的,比如计算机性能指标里的 CPU 使用率,要注意数据采集函数本身的成本。psutil.cpu_percent(interval=1)会阻塞 1 秒等待采样,如果你把这个函数直接放进路由,前端每 3 秒请求一次,接口的响应就被拖到 1 秒以上。我一般会把这类采样放到独立线程或定时任务里,路由只负责读取最近一次缓存值,这样接口的响应时间可以保持在个位数毫秒。
4. 多页面切换:模板继承、蓝图路由与 iframe 的取舍
4.1 最常见的做法:Blueprint 分模块加模板继承
这个标题说的大屏支持多页面切换,最踏实的落地方式是 Flask 模板继承加蓝图路由。每个模块一个 Blueprint,公共导航写进base.html,子页面只写自己的内容块,切换页面就是一次普通的 GET 跳转,浏览器前进后退都正常。先看蓝图组织:
# app.py 中注册三个模块的蓝图 from blueprints.dashboard import dashboard_bp from blueprints.air import air_bp from blueprints.performance import performance_bp app.register_blueprint(dashboard_bp, url_prefix="/") app.register_blueprint(air_bp, url_prefix="/air") app.register_blueprint(performance_bp, url_prefix="/performance")注意url_prefix的用法:数据看板挂在根路径,空气质量挂在/air,性能指标挂在/performance。这样每个模块内部的接口可以相对路径组织,不会互相撞车。blueprints/air.py内部只需要关心空气质量自己的数据和页面:
@air_bp.route("/") def index(): return render_template("air.html")模板继承里,公共导航是重点:
<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{% block title %}数据可视化大屏{% endblock %}</title> </head> <body> <div class="nav"> <a href="{{ url_for('dashboard.index') }}" class="{{ 'active' if request.endpoint == 'dashboard.index' else '' }}">数据看板</a> <a href="{{ url_for('air.index') }}" class="{{ 'active' if request.endpoint == 'air.index' else '' }}">空气质量监测</a> <a href="{{ url_for('performance.index') }}" class="{{ 'active' if request.endpoint == 'performance.index' else '' }}">计算机性能指标</a> </div> {% block content %}{% endblock %} {% block scripts %}{% endblock %} </body> </html>子页面用extends把内容塞进公共布局:
<!-- templates/air.html --> {% extends "base.html" %} {% block title %}空气质量监测{% endblock %} {% block content %} <div id="air_pm25" style="width:100%;height:360px;"></div> <div id="air_rank" style="width:100%;height:360px;"></div> {% endblock %} {% block scripts %} <script src="{{ url_for('static', filename='js/air_charts.js') }}"></script> {% endblock %}用request.endpoint判断当前页面,是为了给导航项加高亮样式。这个判断在模板里写很简单,也是我推荐这套方案的原因之一——不需要前端路由、不需要状态管理,一个模板继承就把“多页面切换”这件事解决了。新增一个模块时,复制一个蓝图文件加一个模板,总改动量不超过二十行。
4.2 什么时候会用到 iframe:模块独立与跨页通信
模板继承适合“整个大屏由同一套代码维护”的情况。但有些企业大屏是多个团队各自维护一块看板,比如运营团队管业务数据,运维团队管性能指标,双方各自部署、各自迭代。这种时候互不干扰的 iframe 更现实。
<!-- 宿主页面:通过切换 iframe 的 src 来实现模块切换 --> <div class="nav"> <button onclick="switchPage('/')">数据看板</button> <button onclick="switchPage('/air/')">空气</button> <button onclick="switchPage('/performance/')">性能</button> </div> <iframe id="screen_frame" src="/" style="width:100%;height:calc(100% - 60px);border:0;"></iframe> <script> function switchPage(path) { document.getElementById('screen_frame').src = path; } </script>iframe 方案的优点和坑一样明显。优点是子模块完全可以独立开发、独立部署,宿主页面永远只负责一个src切换。缺点也很暗:iframe 内部的 URL 不会同步到浏览器地址栏,用户刷新宿主页面会回到初始模块;iframe 内部若有轮询请求,切走之后如果不处理,定时器依旧在后台跑。如果宿主页面和 iframe 内部需要共享数据,得用postMessage做跨页通信:
// iframe 内部向宿主页发送消息 parent.postMessage({ type: 'metric', value: 86 }, '*');我自己做多页面切换的顺序是:能用模板继承解决的,绝不上 iframe。只有当某个模块真的需要独立团队维护、或者第三方系统必须嵌入到大屏里时,才会用 iframe 包一层。
4.3 页面切换后的清理:定时器与图表实例的存活管理
这是多页面方案里最隐蔽的坑。用模板继承做切换时,每个页面离开后,它对应的setInterval和 ECharts 实例并不会自动消失。如果用户从数据看板切到空气质量再切回来,数据看板页面里那段setInterval还在后台跑,等切回来时又新开了一个定时器,结果就是两个定时器同时在请求同一个接口,图表更新频率翻倍,内存占用也在涨。
解决办法是给每个页面的轮询加生命周期管理。我的做法是统一用visibilitychange事件来控制:
// static/js/common_polling.js var pollTimer = null; function startPolling(fetchFn, interval) { if (pollTimer) clearInterval(pollTimer); // 每次切回页面时重新开始轮询 pollTimer = setInterval(fetchFn, interval); } function stopPolling() { if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } } // 页面切换到后台时暂停轮询,切回前台时续上 document.addEventListener('visibilitychange', function() { if (document.hidden) { stopPolling(); } else { startPolling(fetchLiveData, 3000); } });ECharts 实例同样需要清理。如果同一个页面被反复进入,echarts.init又创建了新的实例,旧的实例占用的 canvas 和内存不会自动释放。我一般在页面卸载前对实例做dispose:
window.addEventListener('beforeunload', function() { if (chart) { chart.dispose(); // 释放 ECharts 实例绑定的资源 } });这套清理逻辑加上之后,多页面切换才真正做到“切得干净”。否则大屏开着两三天,后台积压几十个定时器和图表实例,内存和 CPU 双双飙高,现场只能重启进程。
5. 大屏避坑指南:五个真实翻车现场与解决办法
5.1 中文乱码与方框字:不只是编码问题
现象:页面标题和数据标签显示成一个个小方框,或者接口返回的 JSON 里中文变成一长串\uXXXX。
原因分两层。\uXXXX是 Flask 的jsonify在部分配置下对非 ASCII 字符做的转义,浏览器解析后其实能正常显示,误以为乱码是调试时看响应体的错觉;真正的方框字是服务器系统缺少中文字体导致的。ECharts 用 canvas 绘制文字,字体渲染依赖操作系统,一个没装中文字体的 Linux 服务器,页面里所有中文都会变成豆腐块。
解决:先确认服务器有中文字体,Debian/Ubuntu 系安装fonts-noto-cjk,CentOS 系安装wqy-microhei。然后在页面 CSS 里声明完整的字体栈:
body { font-family: "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif; }另外,数据库连接层面如果出现中文乱码,优先检查连接字符串是否带了charset=utf8mb4,Python 这边统一用 UTF-8 读写。总之遇到中文问题,先分清是传输层、渲染层还是字体层,再动手改。
5.2 1920 设计稿投到 1366 屏幕:适配三件套
现象:开发时按 1920×1080 设计的布局,拿到 1366×768 的投屏电脑上一看,右侧柱状图被截断,底部指标卡看不见。
原因:页面所有尺寸都用固定像素写死,屏幕变小后内容溢出,没有自动缩放。
解决:大屏适配我习惯用“固定设计稿 + transform 等比缩放”的方案,而不是 rem 或 vw。原因很简单,ECharts 图表里的字号和坐标轴宽度很多是写在 JavaScript 配置里的固定 px,rem 方案管不到它们,等比缩放却能覆盖整个页面。
// static/js/screen_adapter.js function scaleScreen() { var designWidth = 1920; // 设计稿宽度 var designHeight = 1080; // 设计稿高度 var scaleX = window.innerWidth / designWidth; var scaleY = window.innerHeight / designHeight; var scale = Math.min(scaleX, scaleY); // 取较小值,保证不出现滚动条 var app = document.getElementById('screen_container'); app.style.transform = 'scale(' + scale + ')'; app.style.transformOrigin = 'left top'; // 以左上角为缩放原点 } window.addEventListener('resize', scaleScreen); scaleScreen();配套的screen_container需要固定宽高:width: 1920px; height: 1080px;,缩放后视觉尺寸会随屏幕变化。注意transformOrigin一定要设为left top,否则默认以元素中心缩放,整个画面会偏移出屏幕。这个方案的代价是缩放后字体边缘可能轻微变糊,但大屏投屏距离远,肉眼几乎察觉不到。如果你用的是 4K 屏,可以做scaleX和scaleY分别缩放,但一般不建议,比例变形对图表可读性影响更大。
5.3 Flask 自带服务器撑不住多人访问:现场卡死的头号原因
现象:大屏部署到内网后,一个人看完全正常;会议室里同时进来二十个人,页面加载变慢,接口响应从几毫秒变成好几秒,图表轮询开始排队。
原因:app.run()拉起的是 Flask 内置开发服务器,它的设计目标是在本机帮你调试,不是生产承载。哪怕开了threaded=True,也只是让每个请求有独立线程处理,线程调度和高并发下的稳定性远远不够。再加上前面说的轮询机制,二十个客户端每 3 秒各发一次请求,对开发服务器来说就已经是持续压力。
解决:生产部署换成 waitress。跨平台、纯 Python、不需要编译,Windows 和 Linux 内网服务器都能直接跑:
pip install waitress waitress-serve --listen=0.0.0.0:8080 app:app如果部署在 Linux 上,也可以换 gunicorn:
gunicorn -w 2 -b 0.0.0.0:8080 app:app同时记得把debug=True关掉,开发服务器在 debug 模式下还会额外做代码重载和调试器拦截,性能损耗更明显。生产环境我还会把轮询间隔从 3 秒调到 5 秒,这个调整对视觉效果几乎无感,但对服务器的压力下降百分之四十以上。
5.4 改了 JS 不生效:静态资源缓存让你白调试
现象:改完图表配色的 JavaScript 文件,刷新浏览器看到的还是旧配色;清了浏览器缓存才生效,非常影响迭代速度。
原因:浏览器对/static/目录下的文件做了 HTTP 缓存,文件 URL 没变,浏览器直接用了本地缓存,根本不去服务器拉新文件。
解决:开发期可以在 Flask 里强制关闭静态文件缓存,用after_request统一加响应头:
@app.after_request def add_no_cache(resp): resp.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" return resp更推荐的正式做法是给静态文件 URL 加版本号参数。模板里引用 JS 时写成url_for('static', filename='js/air_charts.js', v=2),每次改完文件把v改成新数字,URL 变化浏览器就会重新请求。这个办法在生成环境的副作用最小,不会因为no-store浪费内网带宽。另外提醒一句,开发时开“无痕窗口”可以逃避缓存问题,但这只是自欺欺人,部署到现场一样会踩。
5.5 数据为空时的连锁反应:序列化与断线
现象:某天数据库的数据源临时挂了,接口开始报错。报错里写着TypeError: Object of type datetime is not JSON serializable,或者前端图表直接空白,看不出任何提示。
原因:这类问题往往不是同一处,而是两处。第一处是 Python 查出来的时间字段是datetime对象,jsonify默认不支持直接序列化;第二处是前端fetch拿到异常响应后,直接跳到了.catch,图表保留上一帧数据还好,如果页面刚好是刚加载完,空白就是必然结果。
解决:数据接口层统一把时间字段转成字符串或时间戳,同时做好空值兜底。
# 统一转换时间字段 def serialize_time(value): if isinstance(value, datetime): return value.strftime("%Y-%m-%d %H:%M:%S") return value @dashboard_bp.route("/api/live_data") def live_data(): result = query_recent_data() # 可能是空列表 # 为空时返回一个默认占位结构,避免前端拿到 None if not result: return jsonify({"time": time.strftime("%H:%M:%S"), "value": 0}) return jsonify({"time": serialize_time(result[0]["time"]), "value": result[0]["value"]})前端也要加一层兜底,请求失败时保留上一帧数据而不是清空图表:
fetch('/api/live_data') .then(function(resp) { if (!resp.ok) throw new Error('bad response'); return resp.json(); }) .then(function(data) { chart.setOption(...); }) .catch(function(err) { console.warn('接口异常,保留上一帧画面', err); });这两处处理之后,即使后端数据短暂中断,大屏画面也不会出现白屏,顶多是数据点不再跳动,同时控制台能看到警告。对于企业监控场景,“保持最后状态”比“报错白屏”要体面得多。
6. 让大屏真正用于生产:数据接入替换与验证技巧
6.1 把模拟数据换成真实数据源的落点
前面一直强调模拟数据函数要独立成模块,就是为了最后这一步替换。替换时不要直接在路由里写 SQL,而是把查询封装成函数,保持返回结构和模拟层一致。
# blueprints/air.py 中替换数据源 def get_air_quality(): """真实环境从空气质量数据库读取最近的监测记录""" row = db.execute( "SELECT pm25, pm10, aqi, update_time FROM air_monitor ORDER BY update_time DESC LIMIT 1" ).fetchone() if row is None: # 数据库暂时没有数据时,返回 None 由上层兜底 return None return { "pm25": row["pm25"], "pm10": row["pm10"], "aqi": row["aqi"], "time": row["update_time"].strftime("%H:%M:%S") }返回字段的 key 和 value 的类型一定要和模拟层完全一致,前端代码才不用动。我踩过一次坑:模拟层pm25返回整数,数据库里是Decimal类型,序列化直接报错,最后统一在查询里做了类型转换。如果数据查询比较耗时,建议在数据层加一个三十秒的缓存——大屏的指标不需要每一秒都精确到最新值,三十秒的延迟完全在可接受范围内。
6.2 用 Network 面板验证轮询节奏,别等现场才发现
大屏上线前,我最常做的验证动作不是盯着页面看图表有没有跳动,而是打开浏览器开发者工具的 Network 面板,过滤出 XHR 请求,观察轮询接口。这一步能同时发现三个问题。
第一,看请求节奏是否符合预期。正常情况下应该看到/api/live_data每隔固定的 5 秒出现一次,如果请求时间间隔忽长忽短,说明有别的定时器在干扰,或者接口响应时间本身就超过了轮询间隔。第二,看单次请求的耗时。点击任意一条请求,看它的 Time 列,如果耗时随请求次数逐渐增长,说明后端在处理不过来,优先去查是不是开发服务器没换、有没有慢查询、数据量是不是太大。第三,看响应体的大小。大屏接口返回的数据如果超过了 100KB,就要考虑是不是把没用的历史数据也塞进去了,该截断的截断。
我自己的习惯是验证完这三个点之后,再把浏览器调成 1366×768 的分辨率看一眼缩放效果。整套检查做完再拿去现场,基本就是稳定的。
最后说个我自己的教训。第一次把大屏放到会议室投屏时,演示前两分钟图表卡住不动,当时还以为是数据源的问题,后来发现是生产环境忘了换 waitress,开发服务器在十几台电脑的轮询请求下直接被打满了。后来所有部署都固定走“waitress + 轮询间隔 5 秒 + 数据兜底”这套流程,再没有在演示现场出过类似翻车。这个方案的好处就是足够简单,简单到出问题时你能快速定位,而不是在一个庞大的工程里猜黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取