cover

『 永不结束的歌 』

Tarokiki

1597 字
8 分钟
手把手教你从0开始实现一个天气查询应用

Simple Weather App#

这是我开发的一个轻量级纯前端天气查询应用。它直接对接 OpenWeatherMap 的公开 API,通过简洁的交互流程,让用户能够快速获取全球任意城市的天气实况。

项目地址: GitHub - Elaina2003/simple-weather-app


✨ 功能特性#

  • 全球城市查询:支持输入城市名称获取当前温度、天气图标及详细描述。
  • 精确匹配:支持通过“城市名,国家代码”(如 shenzhen,cn)进行更精确的定位。
  • 智能检测:内置重复城市检测机制,避免在结果列表中添加重复数据。
  • 响应式布局:完美适配手机、平板及 PC 端,确保不同设备上的使用体验。
  • 纯前端架构:无需任何后端支撑,直接在浏览器端处理逻辑与展示。

🛠️ 技术栈#

本项目采用了最纯粹的 Web 前端技术,旨在展示对基础技能的掌握:

  • HTML5:构建语义化的页面结构。
  • CSS3:实现流畅的卡片布局与响应式适配。
  • 原生 JavaScript
    • 使用 fetch API 处理异步网络请求。
    • 动态操作 DOM,实现搜索结果的实时渲染。
    • 处理复杂的字符串解析与错误反馈逻辑。

0. 开发前准备#

天气 API 选择:OpenWeatherMap,这是一个 2014 年成立的专业天气数据平台,提供全球实时、历史、预报天气数据

API Key 获取:需在官网注册账号生成专属 Key,避免共享 Key 触发调用限制。

免费版支持:当前天气 API、5 天 / 3 小时预报 API 等基础能力,满足简易应用开发需求。

天气图标与 UI 资源:使用 OpenWeatherMap 自带的官方图标集,通过 API 返回的图标代码直接调用。

1. 页面结构搭建(HTML)#

采用语义化标签,设计 2 个核心区域,天气卡片通过 JS 动态生成

<!-- 搜索栏区域 -->
<section class="top-banner">
<div class="container">
<h1 class="heading">Simple Weather App</h1>
<form>
<input type="text" placeholder="Search for a city" autofocus>
<button type="submit">SUBMIT</button>
<span class="msg"></span>
</form>
</div>
</section>
<!-- 天气展示区域 -->
<section class="ajax-section">
<div class="container">
<ul class="cities"></ul>
</div>
</section>

2. 样式设计(CSS)#

使用CSS 变量统一配色,结合Flex+CSS Grid实现响应式布局,添加伪元素 / 过渡效果优化视觉体验,全局字体采用 Roboto 无衬线字体。

:root {
--bg_main: #0a1f44;
--text_light: #fff;
--text_med: #53627c;
--text_dark: #1e2432;
--red: #ff1e42;
--darkred: #c3112d;
--orange: #ff8c00;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-weight: normal;
}
button {
cursor: pointer;
}
input {
-webkit-appearance: none;
}
button,
input {
border: none;
background: none;
outline: none;
color: inherit;
}
img {
display: block;
max-width: 100%;
height: auto;
}
ul {
list-style: none;
}
body {
font: 1rem/1.3 "Roboto", sans-serif;
background: var(--bg_main);
color: var(--text_dark);
padding: 50px;
}

搜索栏样式(响应式)#

.top-banner {
color: var(--text_light);
}
.heading {
font-weight: bold;
font-size: 4rem;
letter-spacing: 0.02em;
padding: 0 0 30px 0;
}
.top-banner form {
position: relative;
display: flex;
align-items: center;
}
.top-banner form input {
font-size: 2rem;
height: 40px;
padding: 5px 5px 10px;
border-bottom: 1px solid;
}
.top-banner form input::placeholder {
color: currentColor;
}
.top-banner form button {
font-size: 1rem;
font-weight: bold;
letter-spacing: 0.1em;
padding: 15px 20px;
margin-left: 15px;
border-radius: 5px;
background: var(--red);
transition: background 0.3s ease-in-out;
}
.top-banner form button:hover {
background: var(--darkred);
}
.top-banner form .msg {
position: absolute;
bottom: -40px;
left: 0;
max-width: 450px;
min-height: 40px;
}
/* 小屏适配(≤700px) */
@media screen and (max-width: 700px) {
.top-banner form {
flex-direction: column;
}
.top-banner form input,
.top-banner form button {
width: 100%;
}
.top-banner form button {
margin: 20px 0 0 0;
}
.top-banner form .msg {
position: static;
max-width: none;
min-height: 0;
margin-top: 10px;
}
}

天气展示区样式(多列响应式 Grid)#

.ajax-section {
margin: 50px 0 20px;
}
.ajax-section .cities {
display: grid;
grid-gap: 32px 20px;
grid-template-columns: repeat(4, 1fr);
}
/* 响应式适配 */
@media screen and (max-width: 1000px) {
.ajax-section .cities {
grid-template-columns: repeat(3, 1fr);
}
}
@media screen and (max-width: 700px) {
.ajax-section .cities {
grid-template-columns: repeat(2, 1fr);
}
}
@media screen and (max-width: 500px) {
.ajax-section .cities {
grid-template-columns: repeat(1, 1fr);
}
}
/* 城市卡片样式 */
.ajax-section .city {
position: relative;
padding: 40px 10%;
border-radius: 20px;
background: var(--text_light);
color: var(--text_med);
}
.ajax-section .city::after {
content: '';
width: 90%;
height: 50px;
position: absolute;
bottom: -12px;
left: 5%;
z-index: -1;
opacity: 0.3;
border-radius: 20px;
background: var(--text_light);
}
.ajax-section figcaption {
margin-top: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.ajax-section .city-temp {
font-size: 5rem;
font-weight: bold;
margin-top: 10px;
color: var(--text_dark);
}
.ajax-section .city sup {
font-size: 0.5em;
}
.ajax-section .city-name sup {
padding: 0.2em 0.6em;
border-radius: 30px;
color: var(--text_light);
background: var(--orange);
}
.ajax-section .city-icon {
margin-top: 10px;
width: 100px;
height: 100px;
}

3. 核心功能实现(JavaScript)#

const form = document.querySelector(".top-banner form");
const input = document.querySelector(".top-banner input");
const msg = document.querySelector(".top-banner .msg");
const list = document.querySelector(".ajax-section .cities");
const apiKey = "你的OpenWeatherMap API Key"; // 替换为专属Key
form.addEventListener("submit", e => {
e.preventDefault();
const inputVal = input.value;
// 构造API请求URL
const url = `https://api.openweathermap.org/data/2.5/weather?q=${inputVal}&appid=${apiKey}&units=metric`;
// Fetch API请求天气数据
fetch(url)
.then(response => response.json())
.then(data => {
// 解构API返回的核心数据
const { main, name, sys, weather } = data;
// 构造天气图标URL(OpenWeatherMap官方图标)
const icon = `https://openweathermap.org/img/wn/${weather[0]["icon"]}@2x.png`;
// 创建并渲染城市卡片
const li = document.createElement("li");
li.classList.add("city");
const markup = `
<h2 class="city-name" data-name="${name},${sys.country}">
<span>${name}</span>
<sup>${sys.country}</sup>
</h2>
<div class="city-temp">${Math.round(main.temp)}<sup>°C</sup></div>
<figure>
<img class="city-icon" src="${icon}" alt="${weather[0]["main"]}">
<figcaption>${weather[0]["description"]}</figcaption>
</figure>
`;
li.innerHTML = markup;
list.appendChild(li);
})
.catch(() => {
msg.textContent = "Please search for a valid city 😩";
});
// 重置表单与提示
msg.textContent = "";
form.reset();
input.focus();
});

定制化图标替换#

将 Envato Elements 的 SVG 图标按 OpenWeatherMap 图标代码命名,仅需替换图标 URL:

// 替换原有icon构造代码
const icon = `https://自定义图标地址/${weather[0]["icon"]}.svg`;

关键优化:防止重复请求#

解决同城市重复添加和同名城市跨国家问题,在表单提交后、API 请求前添加校验逻辑:

form.addEventListener("submit", e => {
e.preventDefault();
const inputVal = input.value;
// 重复请求校验逻辑
const listItems = list.querySelectorAll(".ajax-section .city");
const listItemsArray = Array.from(listItems);
if (listItemsArray.length > 0) {
const filteredArray = listItemsArray.filter(el => {
let content = "";
if (inputVal.includes(",")) {
if (inputVal.split(",")[1].length > 2) {
inputVal = inputVal.split(",")[0];
content = el.querySelector(".city-name span").textContent.toLowerCase();
} else {
content = el.querySelector(".city-name").dataset.name.toLowerCase();
}
} else {
content = el.querySelector(".city-name span").textContent.toLowerCase();
}
return content == inputVal.toLowerCase();
});
if (filteredArray.length > 0) {
msg.textContent = `You already know the weather for ${filteredArray[0].querySelector(".city-name span").textContent} ...otherwise be more specific by providing the country code as well 😉`;
form.reset();
input.focus();
return; // 终止后续API请求
}
}
// 以下为原有API请求和渲染代码...
});

功能扩展思路#

  • 地理定位自动查询:调用浏览器 Geolocation API 获取用户当前位置,自动请求周边城市天气数据。
  • 数据持久化:通过 localStorage 存储查询记录,或结合 Firebase 实现实时数据库同步。
  • 天气预报可视化:集成 Highcharts.js 等图表库,生成温度 / 降水等气象趋势图。
  • 天气预警提醒:集成 OpenWeatherMap 的预警 API,当有天气预警时,通过弹窗或通知提醒用户。

🚀 总结#

这个项目虽然体量不大,但它完整地覆盖了前端开发中网络请求、状态管理、DOM 操作以及响应式设计等核心环节。如果你对前端基础不了解,耐心看完,你一定有所收获!


手把手教你从0开始实现一个天气查询应用
https://fuwari.vercel.app/posts/simple-weather-app/
作者
茶饼茶饼
发布于
2026-03-16
许可协议
CC BY-NC-SA 4.0