一直以来,我都想做点“既能练技术、又能自用”的小工具。既然每天都要查天气,那干脆自己动手写一个“天气预报小组件”吧!从获取城市天气,到用 SVG 画温度曲线,再到界面美化、异步数据处理,最后甚至还尝试加入定位和空气质量功能,这一过程让我在实战中重新认识了前端的强大。
这篇文章就带你完整回顾我实现这个小组件的全过程。没有过度分点,也没有冰冷的技术堆砌,有的只是一个开发者从构想到落地的细致旅程。
想做点有趣的,那就从天气说起
最开始我想做个能展示今天气温的小组件,结果写着写着变成了一个可以:
- 输入城市名实时获取天气;
- 显示接下来的三天预报;
- 用 SVG 曲线画出温度变化;
- 背景图还能随天气自动变化;
- 空气质量也能一并显示……
我不是冲着“卷功能”去的,而是在实现过程中不断有了新灵感,比如画温度曲线那一块,原本是想用 ECharts,但觉得太重,就转向用 SVG 自己画,结果收获不小。
一切从架构开始
我的项目采用纯前端实现,核心技术选用 HTML + CSS + JavaScript,数据源来自 OpenWeatherMap 免费 API。整个系统的逻辑结构非常清晰,用一张流程图描述如下:
天气数据的魔法来自哪里?
当然得感谢 OpenWeatherMap 提供的 API,免费版就能满足我们基本需求:
- 实时天气:
https://api.openweathermap.org/data/2.5/weather
- 天气预报(3 日):
https://api.openweathermap.org/data/2.5/forecast
- 空气质量:
https://api.openweathermap.org/data/2.5/air_pollution
你需要去官网注册个账号拿到 API Key,然后在 fetch 请求里拼接这个 Key 就能调用了。
例如,我的请求写成这样:
const apiKey = '你的API_KEY';
const city = 'Shanghai';
const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric&lang=zh_cn`;
fetch(url)
.then(response => response.json())
.then(data => renderWeather(data))
.catch(error => console.error('获取天气失败:', error));
默认温度是 Kelvin,我加了 units=metric
参数改为摄氏度;为了支持中文,把 lang=zh_cn
加进去,完美!
UI 设计:不仅要能用,还得好看
做小组件最大的痛点是不能丑。于是我参考了一些天气 App 的界面风格,最后决定用卡片式设计。主要部分包括:
- 城市搜索输入框;
- 当前天气展示区域(图标、温度、描述);
- 三日天气预报列表;
- SVG 温度曲线;
- 空气质量提示条;
- 背景自动切换(晴、雨、雪等)。
基础 HTML 结构如下:
<div class="weather-widget">
<input type="text" id="cityInput" placeholder="请输入城市名">
<div class="weather-info">
<div id="current-weather"></div>
<svg id="temperature-chart"></svg>
<div id="forecast"></div>
<div id="aqi"></div>
</div>
</div>
而 CSS 方面,我使用了现代设计语言:大圆角、阴影卡片、渐变背景、流畅动画等元素。像下面这段就是搜索框的样式:
#cityInput {
width: 90%;
padding: 12px 16px;
font-size: 1rem;
border: none;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
outline: none;
transition: all 0.3s ease;
}
#cityInput:focus {
box-shadow: 0 0 8px rgba(0,123,255,0.5);
}
有点像 Bootstrap 的味道,但我用的是纯 CSS 实现的。
异步请求与组件状态管理
数据获取是异步的,不能让 UI 卡死,于是我用了 async/await 写法:
async function getWeather(city) {
const response = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric&lang=zh_cn`);
if (!response.ok) {
throw new Error('获取天气数据失败');
}
const data = await response.json();
return data;
}
这比 .then().catch()
的链式结构更优雅,也更易读。每次输入城市后触发这个函数,返回数据后再调用更新 DOM 的方法。
SVG 温度曲线:动手不如动脑再动手
我原本以为画温度图会很复杂,后来发现只要你掌握 SVG 的 <path>
元素,剩下的只是数学。
首先我们把预报的温度变成坐标点:
const points = forecast.map((item, index) => {
const x = index * 80;
const y = 200 - (item.temp - minTemp) * 5;
return { x, y };
});
然后用这些点构造 path:
const pathData = points.map((p, i) =>
i === 0 ? `M${p.x},${p.y}` : `L${p.x},${p.y}`
).join(' ');
document.querySelector('#temperature-chart').innerHTML = `
<path d="${pathData}" stroke="#4fc3f7" fill="none" stroke-width="2"/>
${points.map(p => `<circle cx="${p.x}" cy="${p.y}" r="3" fill="#4fc3f7"/>`).join('')}
`;
就是这样,温度曲线和节点就画出来了,整齐又轻量,不用加载第三方库。
空气质量——让小组件更“健康”
一开始,我简单地把空气质量当成一个额外的数字展示:读取接口,拿到 AQI,显示在卡片底部。但很快我发现,仅仅展示数值并不能直观地传达空气状况;加个醒目的颜色和建议语句,会让用户更容易理解。于是,我给不同 AQI 范围配了不同的背景色,并在旁边加上一段简短的健康建议。整个渲染流程如下(用一段时序图描述):
在代码层面,我新增了一个 getAirQuality
函数,用 async/await 保持风格一致:
async function getAirQuality(lat, lon) {
const url = `https://api.openweathermap.org/data/2.5/air_pollution?lat=${lat}&lon=${lon}&appid=${apiKey}`;
const response = await fetch(url);
if (!response.ok) {
console.warn('获取空气质量数据失败');
return null;
}
const data = await response.json();
return data.list[0];
}
拿到地理坐标后的回调里,我会把 data.main.aqi
传入渲染函数里。渲染时,我在卡片底部放了一个横条,用 CSS 渐变和圆角做成仪表盘的感觉:
#aqarow {
height: 12px;
border-radius: 6px;
background: linear-gradient(
to right,
#55a84f 0%, /* 优 */
#a3c853 25%, /* 良 */
#fff833 50%, /* 轻度 */
#f29c33 75%, /* 中度 */
#e93f33 100% /* 重度 */
);
overflow: hidden;
position: relative;
}
#aqarow::after {
content: '';
position: absolute;
top: -4px;
left: calc(var(--aqi-percent) - 6px);
width: 12px;
height: 20px;
background: #333;
border-radius: 2px;
}
在 JS 里,我把 AQI 值映射到百分比,再用 CSS 变量动态修改横条指针的位置:
function renderAQI(aqi) {
const container = document.querySelector('#aqi');
if (!aqi) {
container.innerText = 'AQI 数据不可用';
return;
}
const percent = ((aqi - 1) / 4) * 100;
container.innerHTML = `
<div id="aqarow" style="--aqi-percent: ${percent}%"></div>
<p>空气质量指数:${aqi} (${aqiMap[aqi].label})</p>
<p>${aqiMap[aqi].advice}</p>
`;
}
aqiMap
是我事先定义好的映射表,里面包含了 label
和简短的 advice
。这样一来,AQI 一目了然,又有实用建议。
背景图自动切换——让界面“活”起来
从一开始,我就在想:天气小组件如果一直是同一个背景,难免单调。于是,我准备了一组晴天、阴天、雨天、雪天等多种天气对应的高清背景图,放在项目的 assets/bg
目录里。不同天气通过 CSS 动画淡入淡出,既流畅又省性能。
为了管理这些背景,我抽象出了一个 updateBackground
函数:
function updateBackground(weatherCode) {
let type = 'default';
if (weatherCode < 600) type = 'rain';
else if (weatherCode < 700) type = 'snow';
else if (weatherCode === 800) type = 'clear';
else if (weatherCode <= 804) type = 'clouds';
const body = document.body;
body.classList.remove('bg-clear', 'bg-clouds', 'bg-rain', 'bg-snow');
body.classList.add(`bg-${type}`);
}
配合下面这段 CSS,就能在切换时出现淡入淡出的过渡效果:
body {
transition: background-image 1s ease-in-out;
}
.bg-clear { background-image: url('assets/bg/clear.jpg'); }
.bg-clouds { background-image: url('assets/bg/clouds.jpg'); }
.bg-rain { background-image: url('assets/bg/rain.jpg'); }
.bg-snow { background-image: url('assets/bg/snow.jpg'); }
我刻意选择了高对比度、饱和度不太高的图,以免干扰文字阅读。几行代码,就让界面瞬间生动了起来。
自动定位——让用户免输入
很多时候,我们只想打开页面就知道本地天气,干嘛还要输入城市?于是我用浏览器的 navigator.geolocation
接口尝试自动获取用户坐标。如果成功,就直接把经纬度传给天气接口;如果失败或用户拒绝,则回退到手动输入模式:
async function initLocation() {
if (!navigator.geolocation) return;
return new Promise(resolve => {
navigator.geolocation.getCurrentPosition(
pos => resolve({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
() => resolve(null),
{ timeout: 5000 }
);
});
}
async function initWeatherWidget() {
const loc = await initLocation();
if (loc) {
const weatherData = await getWeatherByCoords(loc.lat, loc.lon);
renderAll(weatherData);
} else {
// 展示输入框,等待用户输入城市
}
}
这个过程完全无感,又给了用户更多的便利,体验感瞬间提升。
组件化与项目结构
到这一步,代码已经有点臃肿。我决定把功能拆分成几大模块:API 层、渲染层、工具函数层、UI 组件层。项目目录长这样:
/src
/api
weather.js
air.js
/components
Widget.js
SearchBar.js
AQIBar.js
TempChart.js
/utils
dom.js
date.js
/assets
/bg
/icons
index.html
main.js
style.css
下面这张类图展示了主要类之间的关系:
Widget
是整个小组件的入口,负责生命周期管理和各子组件的协调。SearchBar
只做搜索输入和事件派发;TempChart
担负 SVG 绘图;AQIBar
则处理空气质量横条。这样的分层让代码职责单一,后续想加功能(比如日夜模式)也能轻松插到 Widget
里。
在 main.js
里,我只需一行就能把小组件绑到页面上:
import Widget from './components/Widget.js';
document.addEventListener('DOMContentLoaded', () => {
new Widget('#weather-container').init();
});
模块化的好处是:每次调试时,只需关注某个子组件即可,大大提高了开发效率。
在把功能都梳理清楚后,我开始琢磨:光有静态展示还不够,用户体验的“灵魂”往往藏在细节里——比如按下搜索按钮的反馈、温度曲线加载时的动效、空气质量横条指针滑动的动画……如果这些细节都做得顺滑,整个小组件就会给人“用起来很舒服”的感觉。
给界面加点“小惊喜”——动画与过渡
最经典的案例,就是当温度曲线刚绘制完成时,让折线一段段“画”出来。为此,我在 SVG <path>
上加了 stroke-dasharray
和 stroke-dashoffset
的技巧。先在 CSS 里写好过渡属性:
#temperature-chart path {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
transition: stroke-dashoffset 1s ease-out;
}
#temperature-chart.drawn path {
stroke-dashoffset: 0;
}
然后在 JS 中,曲线生成完毕后,给 <svg>
容器加上一个 drawn
类:
function drawTemperatureChart(points) {
const svg = document.querySelector('#temperature-chart');
// ... 生成 <path> 和 <circle> 省略
// 延迟一下,让浏览器先渲染初始状态
requestAnimationFrame(() => {
svg.classList.add('drawn');
});
}
这样一来,用户每次看到折线从左到右“自画”出来,就有一种仪式感,感觉这个组件很有“生命”。
同理,我在搜索输入框聚焦和失焦时,给了额外的阴影增大/减小动画;在空气质量横条指针移动时,也加了 0.5s 的 transition
,让那根小“刻度”滑动起来更加柔和。哪怕是背景图淡入淡出的 1s 过渡,也让我在切换晴天和雨天背景时觉得很自然。
为了更直观地整理这些动画,我画了一张状态机的流程图,描述各个 UI 元素在用户操作和数据更新时的状态迁移:
通过这张图,对比了“普通”与“聚焦”状态下阴影的变化,对应 CSS 中的 box-shadow
参数;也对比了折线在 “未绘制—绘制中—已绘制” 三个阶段的视觉差异。这种状态机思维帮我在写代码前就想清楚了各种边界条件。
性能优化——让小组件跑得更快
有了动画,下一步得确保性能不打折扣。我的目标是在移动设备上也能保持流畅,于是做了几项优化:
-
减少重排与重绘 渲染 SVG 折线和圆点时,我一次性用字符串拼接好所有节点,直接设置
innerHTML
,而不是多次appendChild
。这样能把多次 DOM 操作合并为一次,大幅减少重排次数。 -
节流用户输入 如果用户快速连续输入城市名,频繁触发 fetch 会浪费带宽,并且渲染一堆无用动画也很糟糕。我在
SearchBar
组件里加了 300ms 的节流(throttle),只有当用户停止输入 300ms 后,才真正发起天气请求。 -
图片懒加载 背景图和天气图标是高分辨率的,如果一次性全部加载,首屏会卡顿。我用原生的
<img loading="lazy">
属性,并在 CSS 中先给背景设成纯色渐变占位,等图片加载完毕再切换背景。 -
使用 requestAnimationFrame 所有与动画相关的 JS 更新,都放到
requestAnimationFrame
回调里执行,保证浏览器在每一帧里只做一次渲染,避免性能抖动。
经过这些处理后,我在 Chrome DevTools 的 Performance 面板里测试,在老设备上也不卡顿。
小结:从练手到常用
回头看看,从输入城市名获取天气,到画温度曲线、加入空气质量、背景动效、性能优化,再到自动定位和一键部署,每一个环节都让我收获颇丰。尤其是手写 SVG、善用 CSS 动画、模块化思路,以及打包发布流程,都是前端日常工作中必备的“肌肉”,练起来绝对不虚。
这个小组件不仅是“练手”项目,更是一个完整的前端落地案例。我把项目源码开源到 GitHub,欢迎大家去 star、fork、提 issue,也欢迎你在此基础上继续扩展功能,比如加入分钟级降雨通知、空气污染物细分监测,或者做一个桌面端 Electron 版……无限可能,都等着你去探索。
祝好天气,也祝你编码顺利。