资讯详情

桌面天气应用开发全记录:Electron+Vue从数据源到打包分发

📅 2026/10/13 22:02:57 | 华诺云谱 👁 阅读
桌面天气应用开发全记录:Electron+Vue从数据源到打包分发
做这个桌面版天气预报应用是去年冬天被手机App折磨到忍无可忍之后的决定。每天上班第一件事是盯着手机解锁、开App、等启动广告、找城市……明明就是一个温度数字的事硬要花掉半分钟。后来干脆想与其每天重复这套流程不如自己写一个常驻在桌面上的天气小工具开机就在抬眼就是点一下能看一周的预报。这篇文章我就把从选型、接数据、画界面到打包分发整条链路遇到的坑都摊开讲一遍。适合正准备做第一个桌面应用、又不想走弯路的人也适合已经写完Web应用想试试桌面壳子的人参考。1. 项目定位与整体设计1.1 这个应用到底解决什么问题很多人会问手机里天气App那么全为什么还要桌面版我的回答是手机天气解决的是“出门前查一下”的场景但解决不了“随时瞄一眼”的诉求。桌面应用常驻任务栏或托盘双击就能看到温度、湿度、风速不需要解锁手机、不需要开屏广告、不需要重新定位本质上是一个“信息仪表盘”而不是一个“功能平台”。所以我在立项时就把功能边界划得很清楚核心信息优先当前温度、体感温度、天气现象、最高/最低温必须第一屏就能看到。次要信息可折叠湿度、风向风力、紫外线、气压放进次级区域不影响主视图。预报信息分两级默认展示未来7天的简版预报点击某一天再展开按小时分布的明细。不做社交、不做资讯、不做会员体系一个纯粹的查询工具。这个边界很重要。做桌面应用最忌讳把Web端那套“内容丰富度”搬过来。窗口就那么小用户注意力只有几秒功能一多界面立刻变臃肿。我见过不少同类项目最后都死在功能堆叠上。1.2 技术栈选型背后的取舍逻辑技术选型是开工前最纠结的一步。桌面天气应用本质上是一个“壳子 数据请求 界面渲染”的组合所以候选方案其实很集中Electron、Tauri以及Python系的PyQt。我给自己列了一个对比表顺便把当时真实感受写进去方案打包体积内存占用开发效率踩坑成本Electron150MB上下偏高常驻100MB高Web技术栈生态成熟资料多Tauri5-10MB低中前端 Rust需要懂一点RustPyQt80MB左右中中Python信号槽界面调试偏传统我最终选了Electron Vue。原因很现实我对前端最熟团队平时也都在写Vue组件库可以直接复用不用为了一个小项目去啃Rust。而且Electron的打包工具、自动更新、托盘组件都有现成方案对“尽快做出能用的成品”这个目标来说开发效率就是最大的杠杆。提示如果你的目标平台只有Windows而且平时写C#那直接用WPF也行没必要为了“技术流行度”硬上Electron。桌面工具这件事选自己最熟的技术就是最优解。1.3 功能清单与实现优先级我把功能按“必须做、应该做、可以以后做”分成三档必须做当前天气展示温度、天气现象、风、湿度未来7天预报城市搜索与切换手动刷新与自动刷新最小化到托盘应该做天气通知提醒比如降温、降雨预警生活指数穿衣、洗车、紫外线缓存离线数据断网可看以后再做多主题皮肤地图气象图层全球城市模糊搜索这个分档帮我避免了很多无效时间。比如生活指数原计划第二周做后来发现优先做缓存和托盘收效大得多。做个人项目最大的风险不是能力不够而是范围失控。2. 天气数据源与API选型2.1 主流天气API怎么选天气数据是整个应用的地基。我调研下来可用数据源大概分三类国内免费接口、国内付费商业接口、海外免费开放接口。区别很明显我直接用一张表总结类型稳定性数据字段限流情况适合场景国内免费中偶尔Key失效较全QPS低约10-50次/秒个人项目、小工具国内商业高有SLA全按套餐商业产品海外免费高但访问慢偏少有每日请求上限境外城市、开发者Demo我选的是国内某免费天气数据平台的个人开发者套餐。免费版给了实时天气、7天预报、生活指数几类接口字段也覆盖得比较全最关键的是国内服务器访问速度快不管是开发调试还是日常使用首屏都能在几百毫秒内返回。个人项目选数据源速度优先级应该排第一体验崩了别的都白搭。海外接口要不要考虑我当时也接了一个作为备用。海外接口有一个好处是数据字段命名规范、文档清晰但国内访问延迟普遍在100-300ms以上而且部分接口免费配额对个人来说稍微紧张一点。我的建议是主数据源一定选本地的海外接口只做降级兜底。2.2 请求参数设计与数据结构天气API通常都是HTTPS的GET接口核心参数就几个定位参数城市ID或经纬度、API密钥、数据单位。我用的这个平台定位参数支持城市ID也支持经纬度坐标实测下来用城市ID最省事因为只需要一次城市搜索请求拿到ID后面所有请求都可以复用。请求地址大致长这样https://api.example.com/v3/weather/now ?location101010100 keyyour_api_key选城市时先请求一个城市搜索接口https://api.example.com/v3/geo/city-lookup ?location上海返回的JSON核心部分是城市ID列表。确定ID之后再请求实时天气和7天预报返回结构大概是这样的字段名我做了简化{ now: { temp: 26, feelsLike: 28, text: 多云, windDir: 东南风, windScale: 3级, humidity: 65 }, daily: [ { date: 2025-06-01, tempMax: 30, tempMin: 22, textDay: 多云, textNight: 晴 } ] }字段映射这块有个坑要提一下不同数据源对天气现象的枚举值不一样有的是“多云”有的是“Partly Cloudy”有的是数字编码。如果一开始就把接口返回的原始字段直接塞进界面后期换数据源时会改到吐。我后来统一封装了一个weatherAdapter把所有外部数据结构转换成应用内部的对象模型。2.3 容错与降级策略接口再稳也有抽风的时候尤其是免费版高峰期偶尔返回超时或者报错。我设计了三层降级第一层主数据源正常就返回主数据源。第二层主数据源超时或异常时尝试备用数据源。第三层两个数据源都不可用读取本地缓存的最后一次成功数据并标记“数据可能过期”。三层下来实际使用中用户几乎感知不到接口故障。这也是我特别想强调的桌面应用不是Web请求一失败就弹Toast的形态用户可能开了一整天不关你必须自己消化这些临时性故障而不是把错误抛给用户。另外免费接口的QPS限制很低我实测峰值只能到每秒十几次。自动刷新一定要加节流我设的是每30分钟拉一次实时天气、每2小时拉一次7天预报完全够用。手动刷新也加了1分钟的频率限制防止用户狂点把Key打爆。3. 界面实现与交互细节3.1 布局设计与组件拆解桌面天气应用的窗口我设计成420x680无边框自带圆角阴影更像一个小组件而不是一个浏览器。界面从上到下分四块搜索栏、当前天气区域、7天预报列表、生活指数。组件拆分我遵循了“一组件一职责”的原则SearchBar.vue负责城市搜索、搜索历史展示。CurrentWeather.vue展示大号温度、天气现象、体感温度。DailyForecast.vue7天预报横向或纵向列表每天一个卡片。HourlyForecast.vue选中某天后的逐小时明细。LifeIndex.vue穿衣、洗车、紫外线等指数。SettingsPanel.vue天气单位切换、自动刷新开关。组件拆细了好处很明显调试时可以单独看某个模块状态管理也更清晰。我用的是Pinia单一weatherStore管理当前城市、当前天气数据、预报数据、加载状态和错误状态所有组件都从store里取数据不跨组件传参后期加功能非常轻松。3.2 自动刷新与竞态问题自动刷新本身不难就是setInterval但以下几个细节真的很影响体验定时器建议放在渲染进程的全局生命周期里不要放在某个组件的mounted里否则组件重新挂载时会多出好几个定时器。切换到后台窗口后系统的定时器会被节流极端情况下可能10分钟才触发一次。我的处理方式是监听visibilitychange事件从后台切回来时立即检查一下数据新鲜度超过10分钟就主动刷新。快速切换城市时容易出竞态问题A城市的数据请求还没回来用户又切到B城市A的返回结果可能把界面覆盖成错误数据。解决方式是每个请求带一个递增的序号返回时只有最新序号才允许写入store。这个“请求序号”方案很小但很关键。没加之前我在测试时反复出现“搜了北京显示上海天气”的诡异问题排查了半天才发现是竞态。3.3 托盘与通知的细节处理托盘是桌面工具型应用的灵魂。应用关闭按钮不真正退出而是隐藏到系统托盘鼠标悬停能看到当前温度右键能调出菜单显示主界面、刷新天气、退出应用。托盘图标我直接在Electron主进程里生成用的是nativeImage动态绘制一个圆形底温度数字这样每次刷新后托盘上的数字也会跟着变。实时更新托盘文字这一点实测下来是用户反馈最好的功能。通知也要克制。一开始我把“紫外线偏强”“空气轻度污染”都推了通知结果半天就烦了。后来只保留两类通知未来24小时内有明显降雨或者24小时内降温超过8度。用户需要的不是情报轰炸而是真正影响出门决策的消息。注意Windows下通知需要配置App User Model IDappId否则通知可能不显示或者显示成“Electron”而不是应用名。打包配置里设置appId后开发模式下也要调用app.setAppUserModelId()这个坑掉过一次。4. 核心业务实现全过程4.1 工程初始化与目录结构我直接用了Vite Vue 3 Electron的组合。初始化命令是npm create vitelatest weather-app -- --template vue npm install electron electron-builder vitest目录结构尽量简洁weather-app/ ├─ src/ # 渲染进程源码 │ ├─ components/ # 组件 │ ├─ store/ # Pinia 状态 │ └─ main.ts ├─ main/ # Electron 主进程 │ ├─ index.ts │ ├─ preload.ts │ └─ ipc.ts ├─ index.html └─ electron-builder.yml主进程和渲染进程分离非常重要。Electron 应用的主进程负责窗口管理、系统托盘、原生菜单和网络请求渲染进程就是普通的Vue应用通过预加载脚本暴露的白名单API通信全程不开启Node集成安全性和稳定性都好很多。4.2 主进程代理网络请求天气API如果直接在渲染进程里请求会碰到跨域问题。虽然可以配CORS但免费API通常不允许随便跨域。最干净的做法是让主进程发请求渲染进程通过IPC调用。主进程这边用ipcMain.handle注册方法const { app, BrowserWindow, ipcMain } require(electron) async function fetchWeather(path, params) { const url new URL(https://api.example.com/${path}) Object.entries(params).forEach(([key, value]) url.searchParams.append(key, value)) const res await fetch(url.toString()) if (!res.ok) throw new Error(Weather api error: ${res.status}) return res.json() } ipcMain.handle(weather:current, async (_event, location) { return fetchWeather(v3/weather/now, { location, key: process.env.WEATHER_API_KEY }) }) ipcMain.handle(weather:forecast, async (_event, location) { return fetchWeather(v3/weather/daily, { location, key: process.env.WEATHER_API_KEY }) })API Key放在主进程的环境变量里而不是渲染进程的源码里。这一点直接关系到安全渲染进程打包后是个静态文件JS里写死Key等于把密钥公开了。虽然接口只有查询权限但被别人拿去刷接口也很烦。preload脚本暴露一个最小化接口给渲染进程const { contextBridge, ipcRenderer } require(electron) contextBridge.exposeInMainWorld(weatherAPI, { getCurrent: (location) ipcRenderer.invoke(weather:current, location), getForecast: (location) ipcRenderer.invoke(weather:forecast, location) })渲染进程里调天气就变成一行Promiseconst data await window.weatherAPI.getCurrent(101010100)4.3 渲染进程的状态管理Pinia的store我写成这样export const useWeatherStore defineStore(weather, { state: () ({ location: localStorage.getItem(location) || 101010100, current: null, daily: [], loading: false, error: null, lastUpdated: null }), actions: { async refreshAll() { this.loading true try { const [now, daily] await Promise.all([ window.weatherAPI.getCurrent(this.location), window.weatherAPI.getForecast(this.location) ]) this.current now.now this.daily daily.daily this.lastUpdated Date.now() this.error null localStorage.setItem(weather_cache, JSON.stringify({ current: this.current, daily: this.daily })) } catch (e) { this.error e.message this.restoreFromCache() } finally { this.loading false } } } })这里有个小设计每次成功拉取后立即缓存到localStorage失败时restoreFromCache把旧数据显示出来页面顶部加一行“当前为离线缓存数据”的小角标不打断用户阅读。我实测下来这个角标比弹窗温和得多用户不会被突然的报错打断。4.4 界面渲染的关键细节7天预报列表我用的是自适应高度的纵向卡片组天气图标没有用Unicode字符而是自己画了一套SVG图标。原因是Unicode天气字符在不同操作系统上渲染差异极大Windows下经常显示成奇怪图形换到macOS又是另一套样式。SVG图标虽然前期要花点时间画但跨平台渲染效果完全一致后期换主题也方便。大号温度区域我做了渐变色背景根据当前温度区间自动切换冷色系对应低温暖色系对应高温。这个交互虽然只是视觉层面但用户感知特别强远远扫一眼就知道外面大概什么温度。窗口拖拽使用-webkit-app-region: drag样式注意页面里需要点击的元素要单独设成no-drag否则鼠标事件会被吞掉。搜索框曾经拖拽区域和点击区域冲突后来排查发现就是忘了写no-drag。5. 打包与分发5.1 打包方案选型Electron的打包方案主要是electron-builder和electron-forge两个阵营。我选了electron-builder理由很朴素配置直接简单文档里例子多遇到问题搜一下基本都有答案。electron-builder核心配置appId: com.example.weathereader productName: Weather Reader directories: output: release files: - dist/** - main/** win: target: - nsis icon: build/icon.ico nsis: oneClick: false allowToChangeInstallationDirectory: true整套流程就是先跑vite build把渲染进程打包成静态资源再用electron-builder把主进程脚本、静态资源和运行时一起打包成安装包。两个步骤合起来npm run build npx electron-builder --win nsis5.2 安装包体积与启动速度优化Electron默认打包出来体积很容易突破200MB因为整个Chromium运行时都打进去了。我做了三个动作把体积压到了90MB左右清理掉打包包含里的node_modules中无用的模块尤其是一些调试工具和测试框架。渲染进程启用gzip压缩静态资源dist产物小很多。图标和资源文件压缩ICO只用必要尺寸不要一堆多余的PNG。还有启动速度。Electron应用启动快慢主要受磁盘IO和加载内容体积影响。Vite打包出来的文件本来就是按需加载的入口只有一个index.html和几个chunk实测冷启动1秒左右体感已经接近原生应用。如果项目更大可以考虑把矢量图标、字体等静态资源转成Base64内联减少小文件IO。5.3 跨平台打包要注意的事理论上Electron跨平台但打包最好在目标平台上打包。在Windows上打macOS包需要额外的签名和依赖很容易失败。我这次主力是Windows所以只打了NSIS安装包。如果你要发macOS版建议直接用macOS机器跑electron-builder --mac dmg否则光是签名那关就够折腾的。另外应用签名在国内分发也许不是硬要求但Windows SmartScreen会拦截未签名的安装包提示“不明来源的应用”。我自己用的时候可以点掉但如果分享给身边朋友用这个弹窗会很劝退。临时方案可以让朋友点“更多信息-仍要运行”长期方案还是得买个代码签名证书。我目前用的是自签名手动放行的方式等真正对外发布再上正式证书。6. 常见问题与排查实录6.1 数据不更新了怎么排查这是桌面天气应用最常被反馈的问题。如果你也遇到按下面顺序排查现象可能原因解决方式界面一直显示旧数据定时器没触发检查visibilitychange和系统电源设置手动刷新无效IPC通道没注册检查主进程ipcMain.handle是否有拼写错误日志里请求失败API Key失效或超限登录平台后台看配额换Key缓存覆盖了新数据竞态条件加请求序号只允许最新请求写状态我在开发中踩过最隐蔽的一个坑应用从睡眠中唤醒后定时器触发了一次刷新但网络还在恢复请求超时然后错误处理把缓存数据覆盖成了空。后来在刷新逻辑里加了一个“至少有一帧数据才能覆盖缓存”的校验问题才彻底解决。6.2 打包后白屏或图标不显示开发模式下跑得好好的打包出来却白屏这是Electron新手最常见的症状。核心原因通常是渲染进程加载路径不对开发模式加载的是http://localhost:5173生产模式应该加载file://.../dist/index.html。需要在Vite配置里设置相对路径export default defineConfig({ base: ./, plugins: [vue()] })不设置base的话资源路径会写成/assets/xxx.js在file协议下解析成磁盘根目录自然加载不到。图标不显示的问题通常也是资源路径问题尤其是SVG、字体文件路径一定不要用绝对路径。6.3 内存占用量偏高Electron被吐槽最多的问题就是内存。我实测一个常驻天气应用稳定后大概150MB左右对现代电脑不算大但对比Tauri确实偏高。如果明显超出这个水平检查这三件事是否多个渲染进程没关干净。打开任务管理器看有没有一堆electron.exe的子进程如果是检查是不是有隐藏窗口没销毁。是否有频繁的DOM更新。天气温度每秒变化不大不需要用setInterval刷整个组件改为按需更新单个字段。开发模式下随随便便跑出300-400MB很正常因为devtools和后端服务都在跑别拿开发模式的数字当生产数据。6.4 城市搜索联想慢城市搜索联想的实现方式通常是输入关键字请求模糊搜索接口但不能用户敲一个字就请求一次必须做防抖。我设的是300毫秒防抖实测打字连续输入时最终只触发1-2次请求服务器压力小了界面也不卡。还有一个细节搜索历史要存但只存最近8个城市并且做去重。用户每天切来切去就那几个城市历史列表比模糊搜索更常用。收尾最后再分享一个我自己的体会桌面天气应用做完了对我个人来说它最大的价值不是“学了多少新框架”而是把一整条桌面应用开发的链路完整跑通了需求边界、数据源、界面、状态管理、打包分发、异常处理每一步都不难但每一步都藏着单独看文档发现不了的细节。我后来复盘真正能提升成品质量的事情其实都很琐碎比如离线缓存能不能用、托盘数字到底精不精确、通知会不会乱弹、重装系统后配置还在不在。这些都不在任何一个教程的“核心功能”章节里但恰恰是用户每天都会接触到的体验面。如果你也要做类似的桌面小工具我的建议是第一版先砍掉一半功能先跑通最小闭环然后自己当真实用户用一周把每次让你觉得“别扭”的瞬间记录下来第二版再处理这些别扭。软件的价值不在功能列表里而在这些天天见的细节里。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑