简介面向Python Web开发初学者与前端工程化学习者的轻量级数据可视化大屏实战项目采用Flask搭建后端聚焦多页面切换、实时数据渲染及企业数据监控与业务分析场景内置数据看板、空气质量监测、计算机性能指标等模块可用于练习接口组织、页面布局与图表联动。压缩包共24个文件约1.02MB以py后端脚本、html页面、js交互逻辑、css样式和png/gif图片素材为主并含说明文档、字体及许可文件体量轻便便于本地运行与二次改造。已有104人学习。项目呈现了Flask多页面路由、数据计算脚本与前端图表渲染的整体结构配合附赠的前端工程化资源可降低环境配置和定制扩展门槛帮助读者理解从数据到可视化界面的实现路径。1. 一块电视大屏引发的选型Flask 轻量方案适合谁前阵子有人问我办公室墙上挂了台电视领导要求实时看到业务数据和几台服务器的状态用现成工具配了半天样式不听话自己写又怕前端工程化那一套太重。这类需求特别典型数据源就三五个接口页面也就三五块屏剩下的全是图表排版和定时刷新。这份基于 Python 和 Flask 的数据可视化大屏系统就是照着这个场景做的包含数据看板、空气质量监测、计算机性能指标三个模块多页面切换后端返回 JSON前端用 ECharts 渲染改一个字典就能把指标换成你自家的业务。它算是一个前端工程化实战项目的精简版后端开发者一个人就能跑通、一个人就能改不用等前端排期。2. 技术选型与目录结构Flask 单应用怎么撑起三块大屏先把定位说死这是一个单进程能跑、不依赖数据库、不依赖 Node 构建链的大屏项目。你下载解压之后pip install一下python app.py就能在浏览器里看到画面。这种“零构建”的取向决定了后面所有的技术选择也决定了它适合什么、不适合什么。2.1 为什么这里选 Flask而不是 Django 或 FastAPI大屏项目的接口数量极少无非是“给我一组空气质量数据”“给我当前 CPU 和内存”一个模块一两个路由就够了。Django 那套 ORM、Admin、中间件栈在这个场景里属于背着你跑步配置成本远大于收益FastAPI 的异步和自动文档很香但它默认不带模板渲染生态你要么前后端分离再起一个前端服务要么自己接 Jinja2对大屏这种“一个 HTML 文件里塞图表”的形态反而绕远了。Flask 的匹配点在于三件事Jinja2 模板开箱即用render_template直接把页面吐出来蓝图Blueprint能把页面路由和 API 路由拆开模块边界清楚应用工厂模式让配置可切换测试环境和生产环境不打架。我把三者的差异列成一张表你按自己的场景对号入座维度FlaskDjangoFastAPI起步成本一个文件即可运行需要工程脚手架需要 uvicorn 启动模板渲染内置 Jinja2内置 DTL无需自行接入适合的接口量几个到几十个几十到上百个几十到上百个大屏这类场景刚好偏重偏分离还有一点容易被忽略大屏往往部署在内网一台普通机器上甚至是一台工控机。Flask 自带开发服务器能直接演示生产换个 WSGI 服务器就行中间不需要 Node、不需要打包产物目录运维那边拷贝一个文件夹就完事。这个“交付物简单”的收益在内部工具里比性能重要得多。2.2 目录结构模板、静态资源和数据接口的边界在哪拿到源码包先别急着跑把目录扫一遍心里有张地图后面改起来才不迷路。这个项目的结构大致是这样组织的flask-dashboard/ ├── app.py # 应用工厂 启动入口 ├── config.py # 配置项集中管理 ├── requirements.txt # 依赖清单 ├── api/ │ ├── __init__.py # 蓝图注册 │ ├── pages.py # 页面路由/、/air、/perf │ ├── air.py # 空气质量数据接口 │ └── system.py # 计算机性能指标接口 ├── services/ │ ├── air_service.py # 空气质量模拟数据生成 │ └── sys_service.py # psutil 采集与降级逻辑 ├── templates/ │ ├── base.html # 公共骨架导航、头部、脚本引入 │ ├── dashboard.html # 数据看板 │ ├── air.html # 空气质量监测 │ └── perf.html # 计算机性能指标 └── static/ ├── css/dashboard.css # 大屏样式、栅格、字体 ├── js/chart-air.js # 各页图表初始化与轮询 ├── js/chart-perf.js └── vendor/echarts.min.js几个边界值得说清楚。api/只负责收请求、调服务、返回 JSON不写业务计算services/放数据来源模拟数据和真实采集都在这一层将来换成数据库或第三方接口只动这里templates/里每块屏一个模板但都继承base.html导航和公共脚本只写一遍。这种分法不是教科书式的洁癖而是为了让你加第四块屏的时候知道新文件该往哪放。static/vendor/单独放 ECharts 是有意为之。很多示例项目图省事直接在模板里写个 CDN 地址本地开发时一切正常一部署到没有外网的内网机器上图表全部白屏。把库文件落到本地目录虽然仓库大了一点但省掉后面一整轮排查。2.3 应用工厂与配置启动入口只有一行命令app.py用的是应用工厂模式核心就是把“创建应用”和“注册组件”拆成函数方便测试时构造不同配置的实例# app.py from flask import Flask from config import Config from api.pages import page_bp from api.air import air_bp from api.system import sys_bp NAV [ {key: dashboard, label: 数据看板, url: /}, {key: air, label: 空气质量, url: /air}, {key: perf, label: 性能指标, url: /perf}, ] def create_app(config_objectConfig): app Flask(__name__) app.config.from_object(config_object) # 三个蓝图分开注册页面和接口互不干扰 app.register_blueprint(page_bp) app.register_blueprint(air_bp) app.register_blueprint(sys_bp) # 把导航配置注入所有模板避免每个视图重复传参 app.context_processor def inject_nav(): return {nav_items: NAV} return app app create_app() if __name__ __main__: # host 设 0.0.0.0 才能被同网段的大屏机器访问 app.run(host0.0.0.0, portapp.config[PORT], debugapp.config[DEBUG])这里面有几个参数值得留意。host0.0.0.0是必须的很多人本地127.0.0.1跑得好好的一到大屏机器上访问不了就是因为只监听了回环地址。debug从配置里读而不是写死True是为了防止哪天把调试模式带上生产——调试模式下 Werkzeug 的控制台是有代码执行能力的内网也不该开。context_processor那几行是我个人比较喜欢的小技巧。导航项如果每个视图都render_template(..., nav_itemsNAV)三块屏还好到十块屏就是纯体力活还容易漏。用上下文处理器统一注入模板里直接{% for item in nav_items %}就行。config.py里通常集中放这么几项PORT、DEBUG、REFRESH_INTERVAL前端轮询间隔单位毫秒由模板渲染给 JS、USE_MOCK是否强制使用模拟数据方便没有 psutil 权限的环境演示。把刷新间隔做成配置而不是写死在 JS 里是因为演示给领导看和长时间挂着看需要的节奏完全不一样前者可以 2 秒一跳显得“很实时”后者 5 到 10 秒更合适避免无谓的请求量。3. 核心链路拆解一次刷新从路由走到 ECharts 渲染大屏看起来是“一堆图在动”实际拆开就三件事页面怎么切、数据从哪来、图表怎么更新。这一章按请求的走向走一遍把每段代码和参数讲透。3.1 多页面切换Jinja2 模板复用与导航状态这个项目采用的是服务端渲染的多页面方案而不是单页应用。点击顶部导航浏览器真的会跳转一次地址拿到一个新的 HTML。这个选择有点“老派”但放在大屏场景里反而稳每块屏的脚本互相隔离一块屏的定时器出问题不会拖垮另一块刷新也只需要按 F5。页面路由集中在一个蓝图里# api/pages.py from flask import Blueprint, render_template page_bp Blueprint(pages, __name__) page_bp.route(/) def dashboard(): # active 用于导航高亮和 NAV 里的 key 对应 return render_template(dashboard.html, activedashboard) page_bp.route(/air) def air(): return render_template(air.html, activeair) page_bp.route(/perf) def perf(): return render_template(perf.html, activeperf)active这个参数是导航高亮的开关base.html里靠它决定哪个 tab 亮起!-- templates/base.html 片段 -- nav classtop-nav {% for item in nav_items %} a href{{ item.url }} class{{ active if active item.key else }}{{ item.label }}/a {% endfor %} /nav main classscreen-body {% block content %}{% endblock %} /main script src{{ url_for(static, filenamevendor/echarts.min.js) }}/script {% block scripts %}{% endblock %}子模板只写自己那块内容公共的导航、ECharts 引入、字体样式全部在base.html里。这样加第四块屏的成本就是新建一个 HTML、继承 base、在NAV里加一行、在pages.py里加一个路由。四步闭着眼都能做。这里有个容易翻车的点模板里引入静态文件一定用url_for(static, filename...)别手写/static/js/xxx.js。倒不是路径错了而是将来你要把应用挂在/dashboard/这样的子路径下时手写的绝对路径全会 404url_for会跟着APPLICATION_ROOT自动变。如果你坚持要单页切换点导航不刷新页面那就得自己接管 history 和组件的销毁重建图表实例不dispose就会内存泄漏。大屏这种“挂三个月不动”的场景我一般反而不折腾单页服务端多页面更省心。3.2 数据接口层空气质量模拟与 psutil 真实采集两个数据源性质完全不同空气质量是外部数据演示时通常没有真实传感器只能模拟计算机性能指标是本机真实数据用psutil直接读。项目把两者都收在services/下面接口层只做转发。空气质量模拟不能简单random.randint那样画出来是锯齿一眼假。常见做法是“基线 正弦波动 小噪声”让曲线有趋势感# services/air_service.py import math, random, time def gen_air_series(points24, base_aqi45, amp18, noise4): 生成一条像样的 AQI 曲线 points: 数据点个数和前端 X 轴刻度数对应 base_aqi: 基准值决定整体处于优还是良 amp: 正弦振幅控制日内波动幅度 noise: 随机噪声避免曲线过于平滑失真 now time.time() series [] for i in range(points): # 用小时做相位模拟一天内的早晚高峰 phase (now - (points - i) * 3600) / 86400 * 2 * math.pi value base_aqi amp * math.sin(phase) random.uniform(-noise, noise) series.append({ ts: int((now - (points - i) * 3600) * 1000), # 毫秒时间戳 aqi: max(0, round(value, 1)), }) return series参数里points24对应 24 小时amp调大曲线起伏更明显noise调大更像真实传感器的抖动。真接了传感器把gen_air_series换成查库或调外部接口即可接口层一行不用改——这就是把数据来源收在 service 层的好处。性能指标这头用psutil# services/sys_service.py import psutil, time, random def snapshot(): try: # interval0.5 表示阻塞采样半秒比 intervalNone 的首次调用靠谱 cpu psutil.cpu_percent(interval0.5) mem psutil.virtual_memory().percent disk psutil.disk_usage(/).percent return {cpu: round(cpu, 1), mem: round(mem, 1), disk: round(disk, 1), ts: int(time.time() * 1000), source: real} except Exception: # 容器或受限环境下读不到降级成模拟值保证大屏不空白 return {cpu: round(random.uniform(20, 70), 1), mem: round(random.uniform(40, 80), 1), disk: 55.0, ts: int(time.time() * 1000), source: mock}注意interval0.5这个写法。psutil.cpu_percent()不带参数时第一次调用永远返回 0.0因为它是拿“上一次调用到这一次调用”的差值算的没有上一次就是 0。很多人在本地调试时看到 CPU 一直是 0以为库坏了其实是这个语义。加一个短采样窗口每次请求都能拿到有意义的数值代价是接口多阻塞半秒——大屏三秒刷一次完全能接受。source字段是我习惯加的前端可以在角落里显示一个小标记告诉你现在是真实数据还是降级模拟。不然哪天接口权限出问题大屏上数字照跳你以为一切正常实际全是假的这种“看起来很健康”的故障最难查。接口本身非常薄# api/air.py from flask import Blueprint, jsonify from services.air_service import gen_air_series air_bp Blueprint(air, __name__, url_prefix/api) air_bp.route(/air) def air_data(): return jsonify({code: 0, data: gen_air_series(points24)})统一返回{code: 0, data: ...}这层壳是为了将来加错误码和提示信息时不用改前端解析逻辑。url_prefix/api把所有数据接口圈在一个前缀下反向代理时只转发/api/到后端、静态资源交给 Nginx配置起来干净。3.3 前端渲染ECharts 实例管理与定时轮询图表这块新手最容易写成“每次刷新都重新 init 一个实例”跑十分钟浏览器就卡死。正确姿势是实例只创建一次之后只更新数据。// static/js/chart-air.js (function () { const dom document.getElementById(air-chart); if (!dom) return; // 页面没这块图就直接退出 let chart echarts.init(dom); // 全页只 init 一次 let timer null; function render(list) { const xAxis list.map(i new Date(i.ts).toLocaleTimeString(zh-CN, { hour: 2-digit, minute: 2-digit })); const values list.map(i i.aqi); chart.setOption({ xAxis: { type: category, data: xAxis }, series: [{ name: AQI, type: line, smooth: true, data: values }] }, true); // 第二个参数 true 不合并整份替换 } function load() { fetch(/api/air) .then(r r.json()) .then(res { if (res.code 0) render(res.data); }) .catch(err console.warn(取数失败保留上一帧, err)); // 失败不清图 } load(); timer setInterval(load, window.REFRESH_INTERVAL || 3000); // 窗口尺寸变化时重算画布大屏切分辨率必踩 window.addEventListener(resize, () chart.resize()); // 页面卸载前清掉定时器别让它继续打接口 window.addEventListener(beforeunload, () { if (timer) clearInterval(timer); chart.dispose(); }); })();逐点说。echarts.init(dom)放在 IIFE 里执行一次实例存在闭包里外部拿不到也就不会误创建。setOption(option, true)的第二个参数notMerge设成true表示这次配置整体替换上一次而不是深度合并——对折线图这种数据长度固定的场景更安全否则旧的 series 可能残留在内存里。fetch的catch里我没有清空图表而是保留上一帧数据。大屏挂在墙上网络抖动一次就变白板观感很差保留旧数据加个角标提示比白屏体面得多。这是一个取舍你要做告警大屏可能就希望明确显示“数据中断”。beforeunload里clearInterval加dispose是防内存泄漏的最后一道。虽然浏览器关页面会回收但用户在多页面之间来回切的时候旧页面的定时器如果不清理会继续打接口你会在后端日志里看到请求量翻倍。还需要补一句分辨率适配。大屏通常是 1920×1080 甚至 4K本地开发用 1366 的笔记本看会挤成一团。常见两种做法CSS 用vw/vh写尺寸或者整页套一个transform: scale()按比例缩放。项目里用的是前者简单直接后者在需要严格等比还原设计稿时更好用但要注意缩放后的坐标换算图表里的 tooltip 位置可能会偏。3.4 实时性权衡轮询、SSE 与 WebSocket 的取舍“实时数据渲染”这个词很容易让人上头一上来就想着上 WebSocket。实际选型得看数据的更新频率和一致性要求方案实现成本服务端压力适用场景定时轮询极低一个 setInterval与页面数成正比3 秒以上刷新指标类大屏SSE低一个流式响应每连接占一个 worker单向推送秒级更新WebSocket中需双向协议处理需异步服务器双向交互、高频推送这个项目默认是轮询REFRESH_INTERVAL一般设在 3000 毫秒。它有一个巨大优势无状态。服务端重启、网络断开、页面挂起下一轮请求自动恢复不用写重连逻辑。真要往秒级推SSE 是性价比最高的升级服务端几行就够# api/system.py改造示例 import json, time from flask import Blueprint, Response, stream_with_context from services.sys_service import snapshot sys_bp Blueprint(sys, __name__, url_prefix/api) sys_bp.route(/stream) def stream(): def gen(): while True: yield fdata: {json.dumps(snapshot(), ensure_asciiFalse)}\n\n time.sleep(2) return Response(stream_with_context(gen()), mimetypetext/event-stream)前端换成new EventSource(/api/stream)即可收到消息就更新图表。但这里埋着一个坑每个 SSE 连接会长期占用一个 WSGI worker用同步模式的 gunicorn 起 4 个 worker连到第 5 个页面就卡住了。要么改异步 workergevent/eventlet要么老老实实轮询。这个坑在第 5 章还会展开。4. 本地跑起来从依赖安装到生产部署的完整步骤4.1 虚拟环境与依赖清单依赖很少requirements.txt里就四样Flask psutil python-dotenv gunicorn我不锁版本号是有原因的这类小项目用到的都是 Flask 最基础的 APIBlueprint、render_template、jsonify2.x 和 3.x 之间没有破坏性差异锁死版本反而容易在某些 Python 版本上装不上。你如果要做交付建议装完之后pip freeze requirements.lock.txt留一份实际版本。安装走三步cd flask-dashboard python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install -r requirements.txtWindows 用户注意虚拟环境激活脚本的路径不一样PowerShell 下第一次执行可能提示脚本被策略禁止改成Set-ExecutionPolicy -Scope Process RemoteSigned再激活就行只影响当前窗口不动系统全局设置。如果下载速度慢加-i换一个国内镜像源。这一步没必要纠结能装上就行。psutil在 Linux 上编译安装偶尔需要 Python 头文件如果报gcc相关错误先装python3-dev不同发行版包名略有差异再重试。这是唯一一个可能卡住的依赖。4.2 启动参数与常见配置项python app.py看到Running on http://0.0.0.0:5000就说明起来了。用同一网段的另一台机器访问http://你的IP:5000能出画面才算真的通了。这一步别跳过本地localhost能开、跨机器打不开是网络和防火墙问题早点暴露比演示当天暴露好。配置项对照配置项默认值作用PORT5000监听端口被占用时改成 5001 等DEBUG开发环境 True热重载 错误页生产必须关REFRESH_INTERVAL3000前端轮询间隔毫秒USE_MOCKFalse强制走模拟数据便于无权限环境演示REFRESH_INTERVAL是渲染进模板供 JS 读的通常在base.html里写一行scriptwindow.REFRESH_INTERVAL {{ config.REFRESH_INTERVAL }};/script。这样改配置只改config.py不用去翻 JS 文件。端口被占用的情况很常见尤其你之前跑过别的 Flask 项目。Linux 下lsof -i:5000找到进程kill掉或者干脆换个端口。别用kill -9一把梭先看看到底是谁占着。4.3 生产部署gunicorn 反向代理的注意点开发服务器不能拿去挂大屏它单线程、无守护、日志也不规范。生产上换成 gunicorngunicorn -w 1 --threads 8 -b 0.0.0.0:8000 app:create_app()这里-w 1单 worker是刻意的。原因有两个一是模拟数据在部分实现里会放在进程内存里维护多 worker 各自持有一份页面刷新一次数据跳一个样看起来像“数据在乱跳”二是如果你真上了 SSE多 worker 会让连接分散广播变得麻烦。--threads 8保证并发处理能力足够大屏这种场景最多也就十几个页面同时在线绰绰有余。前面挂 Nginx 做反向代理和静态资源直出配置大致是server { listen 80; server_name _; location /static/ { alias /opt/flask-dashboard/static/; # 静态文件不走 Python expires 7d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 300s; # 给 SSE 留足时间 } }静态资源交给 Nginx 直出能把 Python 进程解放出来尤其是 ECharts 那个 1MB 左右的库文件每次都过 Flask 没必要。proxy_read_timeout默认是 60 秒如果你用了 SSE60 秒没数据就会被掐断页面表现是“推了几条就不动了”很多人会误以为是前端逻辑写错。最后配个开机自启用 systemd 写个 unit 文件Restartalways。大屏这种东西没人天天看着进程挂了要能自己起来。我自己在工控机上部署时就吃过一次亏周末断电重启周一早上大屏黑着脸从那以后凡是挂墙上的服务我一律配自启。5. 避坑记录这几处翻车我印象最深5.1 图表白屏控制台报 echarts is not defined现象页面框架出来了导航也在但所有图表区域一片空白F12 控制台第一条报错就是Uncaught ReferenceError: echarts is not defined。原因模板里用的是 CDN 地址部署到内网机器后外网不通脚本加载失败。或者脚本标签写在图表初始化代码之后执行顺序错了。解决把echarts.min.js下载下来放进static/vendor/用url_for引入并且确认它在{% block scripts %}之前。改完之后在断网的机器上再验一遍别在能上网的开发机上点头。5.2 内存越跑越高页面切几次就卡现象大屏挂了两天Chrome 内存占用从 200MB 涨到 1GB 以上切换页面明显卡顿。原因图表实例和定时器没有释放。每次进入页面init一个新实例旧的实例还挂在 DOM 上定时器也是切走之后还在跑请求数翻倍。解决把实例存在闭包里只建一次beforeunload里clearIntervalchart.dispose()。验证方法是打开开发者工具的性能面板反复切换页面十次看内存曲线是不是回到基线。如果还涨检查有没有在setOption里每次都新建 series 对象。5.3 接口返回 200前端却解析失败现象fetch(/api/air)走到.json()报Unexpected token 但看网络面板状态码是 200。原因返回的其实是 HTML。常见于两种情况——路由没匹配上被 404 页面兜住了或者视图函数里写了render_template而不是jsonify复制粘贴时改漏了。还有一种隐蔽的蓝图注册时url_prefix写重了实际路径变成/api/api/air。解决先在浏览器地址栏直接访问接口 URL看返回的是 JSON 还是 HTML。是 HTML 就顺着蓝图注册和路由装饰器往下查。养成习惯接口层所有视图返回值只用jsonify不要在同一个蓝图里混着写模板渲染。5.4 容器里 CPU 指标恒为 0现象本地跑得好好的一放进容器CPU 使用率永远是 0.0 或者一个不变的固定值内存倒是正常。原因容器内的/proc和cgroup视图受限psutil.cpu_percent()在部分环境下拿不到有效的两次采样差值。interval0.5能缓解一部分但某些精简镜像里仍然失效。解决try/except兜底降级到模拟值同时在返回里带上source字段让前端显示数据来源。更好的做法是读 cgroup 的cpu.stat自己算但那属于另一个话题了。关键是别让大屏显示一个恒定的 0那比显示“模拟数据”更误导人。5.5 gunicorn 多 worker 后数据来回跳现象同一个页面刷两次看到的曲线不一样数值上下乱窜像是数据源不稳定。原因-w 4起了四个 worker空气质量模拟数据如果是在进程内维护的随机种子或序列每个 worker 各自生成一份请求被负载到哪个 worker 就看到哪一份。解决把 worker 数降到 1用多线程扛并发或者把状态外置到 Redis 之类的共享存储。验证方法很简单同一个接口连打十次看返回的ts序列是否连续递增跳变就说明请求落到了不同进程。6. 二次开发加一块业务屏的正确姿势拿到这套代码绝大多数人的下一步都是“换成我自己的业务指标”。改动路径其实很固定我按顺序列一下步骤改哪个文件具体动作1services/xxx_service.py新建服务返回你业务的数据结构2api/xxx.py新建蓝图包一层{code:0,data:...}3app.py注册蓝图往NAV里加一行4templates/xxx.htmlstatic/js/chart-xxx.js复制一份现有页面改图表配置第 1 步是最容易做歪的地方。很多人上来就改接口层把数据库查询直接写在视图函数里改着改着三块屏的代码互相纠缠。正确做法是先把服务层写好让它无论数据从哪来对外都吐同一种结构一组带时间戳的对象数组。这样前端图表代码几乎不用动换业务只换服务实现。第 2 步注意蓝图名不要重复Blueprint(xxx, __name__, url_prefix/api)里的名字是全局唯一的重名会在注册时报错。第 4 步复制模板时记得把{% block scripts %}里引用的 JS 文件名一起改掉否则两块屏会共用同一个脚本图表串数据。改完怎么验证我的习惯是三步先直接访问接口 URL 看 JSON 结构对不对再单开一个页面只看图表静态渲染把轮询关掉最后开启定时器挂十分钟看内存和请求数是否平稳。跳过第一步直接看页面出问题时你会分不清是数据错了还是渲染错了。从那以后我每加一块屏都先把接口返回的 JSON 结构和前端setOption里的字段对齐写在一张纸上再动手敲代码。这个笨办法帮我省掉了至少一半的联调时间。希望帮到你。本文还有配套的精品资源点击获取