1. 为什么我们需要一个桌面版天气应用?
在智能手机普及的今天,大多数人已经习惯通过手机查看天气信息。但作为一名长期使用多显示器工作的开发者,我发现自己经常需要频繁解锁手机查看天气,这种操作打断了工作流。桌面端天气应用可以解决几个实际问题:
- 多显示器环境下快速获取天气信息(特别是第二/第三屏幕)
- 系统托盘常驻显示,避免频繁切换应用
- 为没有手机的专业场景(如直播、交易室)提供天气数据
- 集成到工作流自动化中(如根据天气自动调整日程)
现代天气API已经非常成熟,比如OpenWeatherMap、WeatherAPI等,提供分钟级更新的全球天气数据。通过Electron等跨平台框架,我们可以用Web技术构建原生体验的桌面应用。
2. 技术选型与架构设计
2.1 核心框架选择
经过对比测试,我最终选择的技术栈组合:
-
Electron:跨平台桌面应用框架
- 优势:使用HTML/CSS/JS开发,支持Windows/macOS/Linux
- 替代方案:Tauri(更轻量但生态较新)
-
React:前端UI框架
- 优势:组件化开发适合天气卡片式布局
- 替代方案:Vue(同样适用)
-
TypeScript:类型安全的JavaScript超集
- 优势:减少运行时错误,提高代码可维护性
2.2 数据源对接
主流天气API对比:
| 服务商 | 免费额度 | 更新频率 | 数据维度 |
|---|---|---|---|
| OpenWeatherMap | 1000次/天 | 每小时 | 基本天气+预报 |
| WeatherAPI | 50万次/月 | 实时 | 空气质量+预警 |
| AccuWeather | 需商业授权 | 15分钟 | 专业气象数据 |
最终选择OpenWeatherMap的One Call API 3.0版本,它提供:
- 分钟级降水预报
- 每小时/每日预报
- 天气警报
- 历史数据
注意:所有天气API都需要注册获取API Key,免费版通常有调用频率限制
3. 开发环境搭建与基础配置
3.1 初始化Electron项目
bash复制# 创建项目目录
mkdir weather-desktop && cd weather-desktop
# 初始化package.json
npm init -y
# 安装核心依赖
npm install electron react react-dom typescript @types/node @types/react @types/react-dom --save-dev
# 安装Electron构建工具
npm install electron-builder --save-dev
3.2 配置TypeScript
创建tsconfig.json:
json复制{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"jsx": "react",
"strict": true,
"esModuleInterop": true
}
}
3.3 主进程基础代码
main.ts基础结构:
typescript复制import { app, BrowserWindow } from 'electron'
let mainWindow: BrowserWindow | null = null
function createWindow() {
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
// 加载React应用
mainWindow.loadFile('dist/index.html')
// 开发模式下自动打开开发者工具
if (process.env.NODE_ENV === 'development') {
mainWindow.webContents.openDevTools()
}
}
app.whenReady().then(createWindow)
4. 核心功能实现
4.1 天气数据获取模块
创建weatherService.ts:
typescript复制import axios from 'axios'
const API_KEY = 'your_api_key_here'
const BASE_URL = 'https://api.openweathermap.org/data/3.0'
interface WeatherData {
current: {
temp: number
humidity: number
wind_speed: number
weather: Array<{
main: string
icon: string
}>
}
daily: Array<{
dt: number
temp: {
min: number
max: number
}
weather: Array<{
main: string
icon: string
}>
}>
}
export async function getWeather(lat: number, lon: number): Promise<WeatherData> {
const response = await axios.get(
`${BASE_URL}/onecall?lat=${lat}&lon=${lon}&exclude=minutely,hourly,alerts&units=metric&appid=${API_KEY}`
)
return response.data
}
4.2 用户位置获取
使用Electron的geolocation API:
typescript复制navigator.geolocation.getCurrentPosition(
(position) => {
const { latitude, longitude } = position.coords
// 调用天气API
},
(error) => {
console.error('获取位置失败:', error)
// 使用默认位置(如北京)
}
)
4.3 系统托盘图标
trayManager.ts实现:
typescript复制import { Tray, Menu, nativeImage } from 'electron'
import path from 'path'
let tray: Tray | null = null
export function createTray(iconPath: string, mainWindow: BrowserWindow) {
const icon = nativeImage.createFromPath(iconPath)
tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{ label: '显示', click: () => mainWindow.show() },
{ label: '退出', click: () => app.quit() }
])
tray.setToolTip('天气应用')
tray.setContextMenu(contextMenu)
// 点击托盘图标显示/隐藏窗口
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
}
5. UI设计与实现
5.1 主界面布局
使用React构建天气卡片组件:
tsx复制import React, { useState, useEffect } from 'react'
const WeatherCard: React.FC = () => {
const [weather, setWeather] = useState<WeatherData | null>(null)
useEffect(() => {
navigator.geolocation.getCurrentPosition(
async (position) => {
const data = await getWeather(position.coords.latitude, position.coords.longitude)
setWeather(data)
},
() => {
// 默认位置
getWeather(39.9, 116.4).then(setWeather)
}
)
}, [])
if (!weather) return <div>加载中...</div>
return (
<div className="weather-container">
<div className="current-weather">
<h2>{Math.round(weather.current.temp)}°C</h2>
<img
src={`http://openweathermap.org/img/wn/${weather.current.weather[0].icon}@2x.png`}
alt={weather.current.weather[0].main}
/>
</div>
<div className="forecast">
{weather.daily.slice(0, 5).map((day) => (
<div key={day.dt} className="forecast-day">
<div>{new Date(day.dt * 1000).toLocaleDateString('zh', { weekday: 'short' })}</div>
<img
src={`http://openweathermap.org/img/wn/${day.weather[0].icon}.png`}
alt={day.weather[0].main}
/>
<div>
<span>{Math.round(day.temp.max)}°</span>
<span>{Math.round(day.temp.min)}°</span>
</div>
</div>
))}
</div>
</div>
)
}
5.2 样式优化
使用CSS Modules实现响应式布局:
css复制/* weather.module.css */
.weatherContainer {
width: 300px;
padding: 20px;
background: rgba(255, 255, 255, 0.8);
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.currentWeather {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.forecast {
display: flex;
justify-content: space-between;
}
.forecastDay {
display: flex;
flex-direction: column;
align-items: center;
font-size: 14px;
}
6. 打包与分发
6.1 配置electron-builder
在package.json中添加:
json复制"build": {
"appId": "com.example.weather",
"productName": "桌面天气",
"directories": {
"output": "dist"
},
"files": ["dist/**/*"],
"mac": {
"category": "public.app-category.weather"
},
"win": {
"target": ["nsis"]
},
"linux": {
"target": ["AppImage"]
}
}
6.2 构建命令
添加构建脚本:
json复制"scripts": {
"start": "electron .",
"build": "webpack --config webpack.config.js && electron-builder",
"pack": "electron-builder --dir",
"dist": "electron-builder"
}
6.3 构建多平台应用
bash复制# Windows
npm run dist -- --win
# macOS
npm run dist -- --mac
# Linux
npm run dist -- --linux
7. 实际使用中的优化技巧
经过几周的日常使用,我发现几个可以提升体验的优化点:
-
内存优化:
- 设置
webPreferences中的backgroundThrottling: false防止窗口隐藏时被节流 - 使用
requestIdleCallback处理非关键UI更新
- 设置
-
数据缓存:
typescript复制// 使用localStorage缓存天气数据 function getCachedWeather() { const cached = localStorage.getItem('weather') return cached ? JSON.parse(cached) : null } function cacheWeather(data: WeatherData) { localStorage.setItem('weather', JSON.stringify(data)) localStorage.setItem('lastUpdated', Date.now().toString()) } -
错误处理增强:
- API调用失败时显示上次成功获取的数据
- 添加重试机制和指数退避
-
主题切换:
tsx复制const [darkMode, setDarkMode] = useState(false) useEffect(() => { document.body.className = darkMode ? 'dark-theme' : 'light-theme' }, [darkMode]) -
通知提醒:
typescript复制function showWeatherAlert() { new Notification('天气变化提醒', { body: '今天下午将有降雨,记得带伞', icon: 'rain-icon.png' }) }
这个项目展示了如何用现代Web技术构建实用的桌面应用。通过合理利用Electron的能力,我们实现了:
- 跨平台支持
- 系统集成(托盘图标、通知)
- 原生性能
- 自动更新(通过electron-updater)
最终的天气应用不仅提供了便捷的天气查询功能,还能根据个人需求进行深度定制,比如添加空气质量显示、穿衣建议等个性化功能。
