天气预报小组件制作

258 阅读9分钟

一直以来,我都想做点“既能练技术、又能自用”的小工具。既然每天都要查天气,那干脆自己动手写一个“天气预报小组件”吧!从获取城市天气,到用 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-dasharraystroke-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 参数;也对比了折线在 “未绘制—绘制中—已绘制” 三个阶段的视觉差异。这种状态机思维帮我在写代码前就想清楚了各种边界条件。

性能优化——让小组件跑得更快

有了动画,下一步得确保性能不打折扣。我的目标是在移动设备上也能保持流畅,于是做了几项优化:

  1. 减少重排与重绘 渲染 SVG 折线和圆点时,我一次性用字符串拼接好所有节点,直接设置 innerHTML,而不是多次 appendChild。这样能把多次 DOM 操作合并为一次,大幅减少重排次数。

  2. 节流用户输入 如果用户快速连续输入城市名,频繁触发 fetch 会浪费带宽,并且渲染一堆无用动画也很糟糕。我在 SearchBar 组件里加了 300ms 的节流(throttle),只有当用户停止输入 300ms 后,才真正发起天气请求。

  3. 图片懒加载 背景图和天气图标是高分辨率的,如果一次性全部加载,首屏会卡顿。我用原生的 <img loading="lazy"> 属性,并在 CSS 中先给背景设成纯色渐变占位,等图片加载完毕再切换背景。

  4. 使用 requestAnimationFrame 所有与动画相关的 JS 更新,都放到 requestAnimationFrame 回调里执行,保证浏览器在每一帧里只做一次渲染,避免性能抖动。

经过这些处理后,我在 Chrome DevTools 的 Performance 面板里测试,在老设备上也不卡顿。

小结:从练手到常用

回头看看,从输入城市名获取天气,到画温度曲线、加入空气质量、背景动效、性能优化,再到自动定位和一键部署,每一个环节都让我收获颇丰。尤其是手写 SVG、善用 CSS 动画、模块化思路,以及打包发布流程,都是前端日常工作中必备的“肌肉”,练起来绝对不虚。

这个小组件不仅是“练手”项目,更是一个完整的前端落地案例。我把项目源码开源到 GitHub,欢迎大家去 star、fork、提 issue,也欢迎你在此基础上继续扩展功能,比如加入分钟级降雨通知、空气污染物细分监测,或者做一个桌面端 Electron 版……无限可能,都等着你去探索。

祝好天气,也祝你编码顺利。