Python Flask + ECharts 自建天气可视化看板:从数据采集到部署全攻略

发布时间:2026/9/8 14:03:30
Python Flask + ECharts 自建天气可视化看板:从数据采集到部署全攻略 昨天早上闹钟响的时候我照例先打开手机自带的天气看了一眼温度又切到另一个App确认降水概率再翻网页版看空气质量。三个来源都说自己最权威但体感温度、风速、降雨概率这些关键信息就是凑不到一块。折腾十分钟之后我得出一个结论——不是没有天气数据是数据散得到处都是。于是我花了两个晚上用 Python Flask ECharts 做了一个网页天气可视化看板打开页面就能看到当前温度、体感温度、24小时温度曲线、降水概率、风速、未来三天预报所有指标同源同页一眼扫完。这篇文章是完整复盘——为什么这么选型、数据接口有哪些坑、图表怎么配置、部署到服务器要注意什么。想学数据可视化或 Python Web 的人或者只是想给家里闲置服务器找个实用小工具的朋友应该都能从中拿走点东西。1. 先想清楚这个天气看板到底要解决什么问题1.1 信息碎片化比没数据更让人头疼每天早晚查天气真正关心的是几个非常实际的问题今天穿什么、出门要不要带伞、适不适合户外跑步、晚上能不能开窗通风。但手机天气App的问题很明显默认页只给一个当前温度和天气图标想看逐小时温度变化要进入二级页面想看降水概率又要再点一个Tab空气质量默认根本不展示。我把手头三个天气App翻了一圈发现它们对同一时刻的温度能差出两度降水概率更是各说各话——因为不同App的数据源、更新时间、预报模型都不一样这不是某一家的问题是整个信息消费方式的缺陷。网页版天气服务更让人头疼。很多天气网站打开先是三秒广告再弹一个登录框好不容易看到温度了页面周围还挂着一堆高温险、天气旅游的推荐位。我就想找一个干净的、把所有关心的天气指标都放在同一屏的页面。自建天气可视化看板本质上不是造一个天气App而是重新组织天气信息让自己只需要一个页面就能完成所有天气判断。这个项目不需要复杂的机器学习也不需要微服务架构核心就是一个完整的采集-清洗-缓存-展示闭环。但恰恰是这种小型全栈项目能把你对前后端数据协作的理解逼出来。1.2 技术选型逻辑为什么是 Flask 和 ECharts 这对组合我自己做的时候先过了三个方案。方案一纯静态 HTML JavaScript 直接请求天气API。这个方案最省事不需要后端但有两个绕不开的问题第一是跨域浏览器直接访问第三方天气接口对方服务端如果没有配置 CORS 或不允许跨域前端拿到数据直接白屏第二是 API Key 暴露把Key写在JS里发到浏览器任何人打开开发者工具就能看到等于把自己的免费额度送给别人刷。这两个问题我在后面都实测踩到了所以纯前端方案直接淘汰。方案二Node.js 做后端。这个完全可行但我当时的数据清洗逻辑、后续可能加的分析脚本都是 Python 写的为了一个天气看板再开一条 Node 技术栈维护成本没必要。方案三就是最终选的 Python Flask 前端图表库。Flask 足够轻量个人项目的路由和模板渲染需求它都接得住没必要上 FastAPI 的异步能力。更关键的是Flask 直接渲染模板页面和后端接口天然同源跨域问题从架构上就消失了。前端图表为什么是 ECharts 而不是 Chart.js 或 D3.jsChart.js 简单但功能偏基础做复杂仪表盘和大屏场景很吃力D3.js 太灵活了从数据绑定到坐标轴都要手写学习曲线很陡。ECharts 是 Apache 开源项目中文文档齐全、生态成熟天气可视化大屏的案例到处都是拿过来改配置就能用对个人项目来说效率最高。1.3 指标体系拆解一张看板应该放什么做可视化最容易犯的错是一上来就想我要做个炫酷的大屏结果图做了一堆自己第二天都不想打开。我这次先列了一张决策清单再倒推要展示什么数据我关心的问题对应的天气指标展示形式今天该穿多厚当前温度、体感温度、24小时温度曲线数字 折线图出门要不要带伞逐小时降水量、降水概率柱状图适不适合户外跑步风速、空气质量折线图 数字未来几天怎么安排每日最高/最低温、白天夜间天气现象卡片列表这张表定下来之后后面所有代码就都围绕它展开。每个图表都必须回答一个具体问题不做为了好看而存在的图。这是整个项目里我觉得最值得分享的思路——先定义信息价值再动手做图。2. 天气数据从哪来API选型、接入流程与返回结构拆解2.1 主流免费天气API对比与选型结论我调研的时候重点看了四个接口整理成一张对比表API免费配额逐小时预报空气质量国内访问体验主要坑点和风天气开发版有每日次数限制具体看官网24h/7d都有有快需要实名认证OpenWeatherMap免费版约60次/分钟有有慢、不稳定海外接口免费档字段受限高德开放平台天气免费限制QPS无逐小时无快字段太简单只有实时和几天预报心知天气免费版次数较少部分有快免费档功能很受限我的结论很直接做个人天气可视化项目国内访问优先选和风天气。它的免费开发版基本覆盖了实时、逐小时、逐天、空气质量、生活指数所有我需要的接口文档全中文字段说明清楚服务器在国内的话延迟也很低。OpenWeatherMap 数据质量其实不错但国内访问体验不稳定对做了放在家里自己用的项目不合适。这里提醒一句各家 API 的免费配额经常调整写代码的时候不要把配额写死把 API Key、城市 ID 都做成配置项以后想换城市或者换数据源只需要改配置不用动代码。2.2 从注册到拿到第一个数据接入流程和风天气的接入流程不算复杂步骤是注册并登录控制台完成实名认证个人开发版需要这一步创建项目选择免费开发版套餐复制项目的 API Key通过城市搜索接口拿到城市 ID这个 ID 是后面所有天气接口的 location 参数很多人第一步就卡在第五步——拿着城市拼音直接去请求天气接口结果返回location is invalid or not found。正确做法是先调城市搜索接口拿数字ID。示例代码很短import requests search_url https://geoapi.qweather.com/v2/city/lookup params { location: 北京, key: YOUR_API_KEY } resp requests.get(search_url, paramsparams, timeout10) data resp.json() for loc in data.get(location, []): print(loc[id], loc[name], loc[adm1])运行后输出类似101010100 北京 北京市这个101010100就是北京的 LocationID。城市ID拿对了后面所有天气接口的请求都不可能再出现找不到位置这种低级错误。2.3 返回结构拆解新手最容易忽略的几个字段实时天气接口${base}/now返回的 JSON 大概是这样的{ code: 200, updateTime: 2025-01-15T20:3508:00, now: { obsTime: 2025-01-15T20:3008:00, temp: 4, feelsLike: -1, icon: 101, text: 多云, windDir: 北风, windScale: 3, humidity: 45, precip: 0.0, pressure: 1020, vis: 10 } }第一次看这个结构我差点被一堆字符串字段绕晕。关键是下面这几个字段的理解字段含义单位注意点temp温度摄氏度返回的是字符串要转数值feelsLike体感温度摄氏度冬天和 temp 差距可能很大humidity相对湿度百分比字符串precip降水量毫米不同接口统计口径不同看文档确认windScale风力等级级不是风速数值是几级风vis能见度公里字符串pressure气压hPa字符串24小时预报接口${base}/24h返回的hourly数组每个元素和now结构类似但多了fxTime预报时间字段格式是2025-01-15T21:0008:00带时区偏移量。这个字段后面画图时会变成 x 轴时间标签处理它的时候有个大坑我会在踩坑章节详细说。3. 后端数据管道定时拉取、缓存刷新与接口设计3.1 项目结构与路由划分后端我用 Flask代码结构保持得很简单weather-viz/ ├── app.py # Flask 入口路由 ├── weather.py # 天气数据获取和缓存逻辑 ├── requirements.txt ├── templates/ │ └── index.html └── static/ ├── css/ │ └── style.css └── js/ ├── chart.js # ECharts 图表渲染 └── main.js # 页面加载逻辑把获取天气数据的逻辑单独放到weather.pyapp.py只负责路由这样职责分离后面想换数据源或者加接口都很方便。Flask 默认会从templates/和static/目录找模板和静态文件所以不需要额外配置目录路径。路由就两个路由方法说明/GET渲染 index.html输出页面/api/weatherGET返回整合后的天气 JSON 数据页面和接口放在同一个服务下是我有意为之。这样浏览器访问的页面和后端接口天然同源不会出现跨域问题部署也简单。个人项目不需要把前后端拆成两个服务来跑。3.2 不写缓存接口迟早被自己刷爆我这个项目第一版没有做任何缓存前端每次打开页面后端就实时请求一次和风天气接口。结果运行两天就发现问题第一个问题是额度。我把项目发给朋友试用几个人同时打开免费版一天几千次的额度肉眼可见往下掉。第二个问题是限流。有朋友反复刷新页面QPS 一高接口就开始返回429 Too Many Requests。天气数据不是实时行情30分钟内的数据基本没有变化完全没有必要每次请求都打到第三方接口。缓存必须做而且我建议不要一上来就上 Redis——个人项目用 Python 字典加时间戳就够了省掉中间件就是省掉故障点。我写了一个非常简单的缓存包装类import time import requests class WeatherStore: def __init__(self, api_key: str, location_id: str, ttl: int 1800): self.api_key api_key self.location_id location_id self.ttl ttl # 缓存有效期默认30分钟 self.cache None self.updated_at 0 def fetch(self) - dict: base https://devapi.qweather.com/v7/weather headers {X-QW-Api-Key: self.api_key} now requests.get(f{base}/now, params{location: self.location_id}, headersheaders, timeout10).json() hourly requests.get(f{base}/24h, params{location: self.location_id}, headersheaders, timeout10).json() daily requests.get(f{base}/7d, params{location: self.location_id}, headersheaders, timeout10).json() return { now: now.get(now, {}), hourly: hourly.get(hourly, []), daily: daily.get(daily, []), update_time: time.strftime(%Y-%m-%d %H:%M:%S), } def get(self, force: bool False) - dict: if force or self.cache is None or time.time() - self.updated_at self.ttl: self.cache self.fetch() self.updated_at time.time() return self.cache这里有个细节值得说get方法带了一个force参数前端可以请求/api/weather?_force1强制刷新一次缓存。平时所有用户都走缓存想手动看最新数据时再强制刷新既保护了额度又保留了一个手动触发的口子。顺便提一个安全习惯API Key 不要硬编码在代码里提交到 Git 仓库。我在项目里放了.env.example文件只写变量名不写值别人 clone 项目后复制成.env填自己的 Key。代码里用os.getenv(QWEATHER_KEY)读取既安全又方便。3.3 数据清洗与接口整形后端多做一步前端少折腾一小时第三方接口返回的字段不是直接能给前端用的我在get()之前加了一层normalize()数据整形逻辑def normalize(self, raw: dict) - dict: now raw[now] hourly raw[hourly] daily raw[daily] def n(v): try: return round(float(v), 1) except (TypeError, ValueError): return 0 return { current: { temp: n(now.get(temp, 0)), feels_like: n(now.get(feelsLike, 0)), text: now.get(text, ), humidity: n(now.get(humidity, 0)), wind_dir: now.get(windDir, ), wind_scale: now.get(windScale, ), precip: n(now.get(precip, 0)), pressure: n(now.get(pressure, 0)), vis: n(now.get(vis, 0)), }, hourly: [ { time: h.get(fxTime, )[11:16], temp: n(h.get(temp, 0)), precip: n(h.get(precip, 0)), humidity: n(h.get(humidity, 0)), wind_scale: n(h.get(windScale, 0)), text: h.get(text, ), } for h in hourly ], daily: [ { date: d.get(fxDate, ), temp_max: n(d.get(tempMax, 0)), temp_min: n(d.get(tempMin, 0)), text_day: d.get(textDay, ), text_night: d.get(textNight, ), } for d in daily[:3] ], update_time: raw[update_time], }这里我做了三件事所有数值字段统一转成float时间字段直接截取成HH:mm格式7天预报只留前3天。这样前端拿到的就是可以直接塞进 ECharts option的数据不用再做一层parseFloat和日期处理。这个后端多做一步前端少折腾一小时的思路在整个项目里让前后端联调的效率高了很多。Flask 接口本身就很薄了from flask import Flask, jsonify, request from weather import WeatherStore app Flask(__name__) store WeatherStore(api_keyYOUR_KEY, location_id101010100) app.route(/api/weather) def weather_api(): force request.args.get(_force, 0) 1 return jsonify(store.get(forceforce)) app.route(/) def index(): return render_template(index.html) if __name__ __main__: app.run(debugTrue, port5000)到这一步后端的数据管道已经畅通了。接下来就是前端怎么把这些结构化的数据变成让人看一眼就懂的天气图表。4. 前端图表落地ECharts 组合出天气大屏4.1 页面布局信息密度和阅读顺序的取舍天气看板不是把图表堆在一起就行还要考虑人眼扫过去先看什么。我的布局是从上到下、从重点到次要顶部是一条醒目的当前天气横条城市、当前温度大字、天气现象、体感温度、更新时间中间是第一张核心大图24小时温度曲线占据页面最宽的列下方是两个并排图左边降水量柱状图右边风速折线图最底部是未来三天预报卡片HTML 骨架大致是这样div classdashboard header classtoday-card div classcity北京/div div classtemp-mainspan idcurrentTemp--/span℃/div div classweather-descspan idweatherText--/span · 体感 span idfeelsLike--/span℃/div div classmeta更新时间span idupdateTime--/span/div /header section classchart-main div idtempChart styleheight: 320px;/div /section section classchart-row div classchart-box h3降水量/h3 div idprecipChart styleheight: 240px;/div /div div classchart-box h3风速/h3 div idwindChart styleheight: 240px;/div /div /section section classdaily-list iddailyList/section /divECharts 的引入我选择下载到本地static/js/vendor/echarts.min.js而不是用 CDN。原因很简单这个服务可能部署在内网或者访问量不大但网络不稳定的环境本地文件不依赖外链页面加载速度更快也不会因为 CDN 被墙或者被劫持导致整个页面图表挂掉。另外关于深色主题还是浅色主题我一开始想做成深色大屏风格看起来确实更有科技感。但深色主题对配色、对比度要求更高调色的时间成本不小。如果不想花太多时间在样式上浅色主题更安全信息可读性也更好。我自己最后选了浅色加蓝色系——天气嘛蓝色系和晴、雨、风的直觉是匹配的。4.2 温度曲线ECharts option 配置逐行解释温度曲线是整个页面的核心图。它把24小时的温度变化连成一条平滑的线人一眼就能看出早上冷、下午热、晚上再降的趋势。核心代码async function initTempChart(data) { const times data.hourly.map(h h.time); const temps data.hourly.map(h h.temp); const chart echarts.init(document.getElementById(tempChart)); chart.setOption({ grid: { left: 50, right: 20, top: 40, bottom: 30 }, tooltip: { trigger: axis, formatter: function (params) { const i params[0].dataIndex; const h data.hourly[i]; return ${h.time}br/温度${h.temp}℃br/天气${h.text}br/湿度${h.humidity}%; } }, xAxis: { type: category, data: times, boundaryGap: false, axisLabel: { interval: 2 } }, yAxis: { type: value, name: 温度(℃), scale: true }, series: [{ name: 温度, type: line, data: temps, smooth: true, symbol: circle, symbolSize: 6, lineStyle: { width: 3 }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: rgba(255, 140, 50, 0.3) }, { offset: 1, color: rgba(255, 140, 50, 0) } ]) } }] }); window.addEventListener(resize, () chart.resize()); }几个配置是实测之后才确定的boundaryGap: false让折线端点从 x 轴最左边开始而不是往中间缩一格视觉上更舒服。scale: true让 y 轴不从 0 开始——如果冬天温度全部在 -5 到 5 度之间y 轴从 0 开始会把曲线压扁在图表底部根本看不出波动。smooth: true画平滑曲线因为温度本来就是连续变化的物理量用平滑线比生硬的折线更直观。自定义tooltip.formatter也很关键。ECharts 默认的 tooltip 只显示温度我想让鼠标悬停时同时看到温度、天气现象、湿度三个信息所以自己写了 formatter。这意味着前端拿到的data.hourly数组里还得带text和humidity字段而这两个字段在数据整形阶段已经准备好了前端写起来很顺。我特别加了window.addEventListener(resize, () chart.resize())。这个很容易被忽略但如果浏览器窗口大小变化时图表没有重新计算尺寸就会出现图表被拉伸变形或者留白的问题。4.3 降水量和风速一张页面怎么放多个不同类型的图24小时温度是线的数据降水量是柱的数据风速是点/线的数据。它们共享同一个时间轴但量纲不同。我的处理方式是各画一张图但把它们放在同一个.chart-row的并排容器里视觉上形成一行双图。降水量柱状图核心配置const precipChart echarts.init(document.getElementById(precipChart)); precipChart.setOption({ series: [{ type: bar, data: data.hourly.map(h h.precip), itemStyle: { color: #4a90d9 }, barMaxWidth: 18 }] });风速图我用了阶梯线const windChart echarts.init(document.getElementById(windChart)); windChart.setOption({ series: [{ type: line, data: data.hourly.map(h h.wind_scale), step: end, lineStyle: { color: #2ecc71, width: 2 } }] });降水用柱状图的原因很直观——柱子的高低对比能让人一眼看出哪个时段雨大。风速这里有个细节接口给的是windScale风力等级几级风它不是连续的物理量所以我用了step: end阶梯线而非普通平滑折线。这个选择背后是语义未来某个时段的风力是4级它在这段时间内应该是一个恒定的台阶画成连续平滑的曲线反而会误导人。如果以后还想在同一张图上叠加更多维度ECharts 支持在同一个实例里配置多个grid、多个xAxis、多个series实现多图联动十字线对齐、数据缩放同步。但我在当前版本里选择一图一容器好处是代码更简单排查问题更直观数据量和图表的对应关系一目了然。4.4 数据加载失败的兜底与手动刷新前端还有一个容易被忽略的点接口失败时的体验。我把 fetch 包成了一个loadWeather函数并在页面右上角放了一个刷新按钮async function loadWeather(force false) { try { const url force ? /api/weather?_force1 : /api/weather; const resp await fetch(url); if (!resp.ok) throw new Error(HTTP ${resp.status}); const data await resp.json(); renderCurrent(data); initTempChart(data); initPrecipChart(data); initWindChart(data); renderDaily(data); } catch (e) { document.getElementById(currentTemp).textContent 加载失败; console.error(e); } }页面加载时自动执行一次loadWeather()用户点刷新按钮时执行loadWeather(true)强制让后端去第三方接口拉一次最新数据。为什么特别强调兜底逻辑因为做可视化项目的时候我们通常只盯着数据正常的时候好不好看容易忽略数据挂了的时候用户看到什么。如果后端接口超时、第三方天气服务临时不可用页面至少应该显示加载失败而不是一片空白。我在开发调试时也靠这个 catch 把错误打到控制台快速定位是后端问题还是前端问题。5. 部署之外的五次翻车跨域、时区、单位与额度陷阱这个项目从能跑到稳定用中间踩了五个比较大的坑。每一个都是真实遇到过、花时间排查过的按现象-排查-根因-修复的方式写出来希望你能绕开。5.1 API Key 放在浏览器 裸奔最开始我图省事在 JavaScript 里直接写了和风天气 API Key前端直接请求第三方接口。页面加载确实很快但一次无意中打开浏览器开发者工具发现 Network 面板里躺着一整条请求 URL最后面清清楚楚带着keyxxxxx的参数。排查链路很简单看到第三方域名请求 → 点开发现 Key 明文 → 意识到只要页面在浏览器里运行任何人按一下 F12 就能复制我的 Key拿它去刷接口额度分分钟耗尽。修复方式就是前面说的前端只请求自己的后端API Key 全部放在服务端环境变量里由 Flask 去请求第三方接口。浏览器永远接触不到 Key。教训是只要数据要经过浏览器就不能把任何凭证放在前端这是底线。5.2 双击 HTML 白屏跨域问题排查项目做到一半我把一个index.html发给朋友让他双击在浏览器里直接打开预览。朋友反馈页面样式都在但所有数据都是加载失败。远程排查时我让朋友打开浏览器控制台看到这样一条报错Access to fetch at http://127.0.0.1:5000/api/weather from origin null has been blocked by CORS policy问题一下就清楚了file://协议的 origin 是null而接口跑在http://127.0.0.1:5000浏览器把这次请求判定为跨域请求直接拦截了。解决方案有两个方向最省事的不要双击 HTML 文件而是通过http://127.0.0.1:5000完整访问页面页面和接口同源永远不跨域。我的项目本来就是 Flask 渲染模板所以这个方案就是最终方案。如果想纯前端部署静态页面就需要在后端加 CORS 中间件比如flask-cors但个人项目没必要为此引入额外依赖。这个坑教会我一件事给人演示项目时一定要用完整的 URL 访问不要发一个孤零零的 HTML 文件。5.3 凌晨4点温度曲线断档时区时间戳的隐形偏移有一天我盯着 24 小时温度曲线看发现凌晨 3 点到 5 点的时间点是错的x 轴标签显示03:00、04:00、05:00但对应数据却对不上整体像被什么东西偏移了。排查过程是先在浏览器把/api/weather返回的原始 JSON 打出来看hourly数组里的fxTime字段发现形如2025-01-15T04:0008:00——这是带时区偏移的 ISO 字符串本来没什么问题。问题出在我最初在后端做了datetime.fromisoformat()再转strftime(%H:%M)在部分环境下这个操作会把08:00当成偏移量做一次时区计算结果凌晨 4 点被转成了前一天晚上 20 点整个序列彻底错位。最终的做法非常粗暴但非常有效永远不要对fxTime做时区转换直接substring截取第 11 到第 16 个字符得到HH:mm。因为天气预报本来就是本地时间接口已经帮你算好了你根本不需要把它再转成用户当前时区。5.4 全是字符串的 JSON前端图表数据错乱的根源有一次我发现温度折线的最高点显示的不是下午两点而是半夜十二点。排查了很久才发现问题出在数据类型上。和风天气接口返回的temp字段是字符串16而不是数字16。前端我用Math.max(...data.hourly.map(h h.temp))算最高温时字符串比较走的是字典序9会被判定比16大所以最高温被算到了晚上九点那个数据上。这个问题的隐蔽性在于ECharts 画线时内部会自动做数值转换所以曲线本身看起来是正常的但一旦我自己对数据做比较、聚合、筛选字符串和数字混用就会产生各种诡异结果。解法就是我前面在数据整形阶段做的所有数值字段在后端统一float()让前端拿到的永远是数字从源头杜绝这类隐患。接口文档里这个字段是 string 还是 number一定要看清楚不能想当然。5.5 免费额度一天就告急多进程缓存的陷阱我实现缓存之后本地测试一直很正常但把服务部署到 Gunicorn 上之后第三方接口请求量突然翻倍了。排查之后发现原因Gunicorn 默认起了多个 worker 进程每个 worker 各自加载了一份WeatherStore实例缓存是进程内的、互不相通的。进程 A 在 10:00 刷新了缓存进程 B 的缓存过期时间一到又会去请求第三方接口实际请求量被 worker 数量放大。这个问题有三个层级的解法最省事的Gunicorn 只开一个 worker-w 1。个人项目完全够用缓存互斥问题从根上消失。更通用的把缓存放到 Redis 或数据库多进程共享但引入中间件会增加运维成本。还有一种是进程间共享内存的方案但配置复杂、收益有限。我选了最务实的第一种。很多教程一上来就让开多个 worker但对个人看板这种低并发场景单 worker 反而是最优解。等访问量真的上来了再上 Redis 也不迟。6. 部署到公网Gunicorn Nginx 的完整配置6.1 Gunicorn 托管 Flask为什么不用 flask runflask run启动的是 Werkzeug 开发服务器它的设计目标是开发调试不是生产环境。具体表现是性能弱、并发能力低、没有专门的进程管理。我的项目要 7x24 小时跑在服务器上第一件事就是换 Gunicorn 托管。安装和启动命令很简单pip install gunicorn gunicorn app:app -b 127.0.0.1:8000 -w 1几个参数说明app:app前面是 Flask 入口文件名后面是 Flask 实例名-b 127.0.0.1:8000只监听本机端口由 Nginx 转发进来不要把 Flask 直接暴露到公网-w 1单 worker原因在 5.5 已经说过了个人项目够用也避免多进程缓存各管各的问题注意不要用--daemon后台运行后面要交给 systemd 管理由 systemd 负责拉起和守护。6.2 Nginx 反向代理一个完整的 server 配置Nginx 在这里做两件事把公网 80 端口的请求转发给本机 8000 端口的 Gunicorn同时托管静态文件。虽然 Flask 也能托管静态资源但 Nginx 处理静态文件性能更好而且可以顺手加缓存头。server { listen 80; server_name weather.example.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /opt/weather-viz/static/; expires 7d; } }配置完成后执行nginx -t检查语法再systemctl reload nginx生效。这里有个经验/static/的alias路径一定要写绝对路径我一开始写相对路径结果浏览器里静态文件全部 404。如果页面能打开但图表加载不出第一件事就是看控制台是不是静态文件 404。6.3 systemd 守护服务器重启后服务自动拉起我不想每次服务器重启都手动敲一遍启动命令所以写了一个 systemd service[Unit] DescriptionWeather Viz Afternetwork.target [Service] Userubuntu WorkingDirectory/opt/weather-viz ExecStart/opt/weather-viz/venv/bin/gunicorn app:app -b 127.0.0.1:8000 -w 1 Restartalways RestartSec5 [Install] WantedBymulti-user.target放到/etc/systemd/system/weather-viz.service然后执行sudo systemctl daemon-reload sudo systemctl enable weather-viz sudo systemctl start weather-vizRestartalways非常关键——进程崩溃或者服务器异常重启后服务会自动恢复不用人工干预。这一点对放在家里跑了半年没管的个人项目来说比任何花哨的功能都重要。6.4 后续可以扩展的几个方向整个项目跑起来之后我实际用了几个月稳定性没什么问题。如果你想在这个基础上继续玩我列几个我自己觉得值得做的方向第一个是多城市切换。现在城市 ID 是写死的其实前端加一个搜索框调后端新增一个/api/city?keywordxxx接口拿到城市 ID 后再刷新所有图表改动量不大。代码结构里缓存类是独立的切换城市只需要重新初始化一个WeatherStore实例。第二个是历史气温对比。把每天的天气数据落库SQLite 就够了积累一个月后可以画出今年气温 vs 去年同期的双线对比。这种功能只有自建项目才方便实现商业天气 App 基本不提供。第三个是天气预警推送。和风天气有预警接口扫描到黄色以上预警就通过消息机器人推一条提醒相当于给自己搭了一个小告警系统。第四个是可视化大屏模式。如果家里有闲置的旧平板或者客厅电视盒子把这个页面全屏展示再配一个深色主题就是一块真正的天气大屏。我在实际使用中最满意的一点是早上不用再点开三个 App 了一个页面扫过去温度、风力、降水趋势全都有数。做这种小工具最大的回报不是技术上的提升而是每天用着的时候都知道它省掉了自己几分钟的重复劳动。这份自己动手的确定感是花钱买不到的。