第6章 6.5 综合实战:天气查询 SPA
上 一章我们折腾了 BOM 那些 API,知道了 window 是浏览器的老大哥、location 掌管地址栏、navigator 能看出用户用啥浏览器。现在你手里有这些工具了,是时候让它们组队干活了——本章我们就用这些知识,做一个真能跑起来的天气查询单页应用(SPA)。
说白了,SPA 就是那种只加载一次的网页,打开后所有的操作都在同一页完成,切换页面不会刷新整个浏览器。天气 App 正好适合做成 SPA:查北京、查上海、查东京,都在一个页面上搞定。
🎯 开场 3 分钟:为什么要学这个?
你有没有遇到过这种情况:
- 每天早上要开好几个网站查天气、查新闻、查限行尾号
- 想做个「每日推送」的小工具,但不知道怎么把数据搬到网页上
- 想做个小项目放简历上,但网上都是 "Hello World" 级别的教程,没法展示真实能力
天气查询 SPA 刚好能解决这些问题:
- 学会用
fetch从免费 API 拿数据 - 学会用
localStorage\n\n
\n\n
\n\n 把数据存在浏览器里 - 学会用 DOM 操作动态更新页面
学完这章,你就能写出一个「输城市名 → 显示天气 → 自动缓存」的真实小工具,发到 GitHub 上绝对比 "图书管理系统" 亮眼。
🧱 基础 25 分钟:核心概念
什么是 fetch?—— 点外卖的比喻
想象你住在一个没有外卖软件的年代。想吃火锅,你得自己打电话给店家,店家说「稍等我去厨房看看」,你只能举着电话等着。
fetch 就是浏览器里的「外卖软件」。你告诉它「帮我拿这个 URL 的数据」,然后它去网络抓数据,抓完了自动回调通知你,中间你可以干别的事(这叫异步)。
// 打个比方:给外卖小哥一个地址,等他送餐上门
fetch('https://api.example.com/weather?city=北京')
.then(response => response.json()) // 解析 JSON 数据
.then(data => console.log(data)); // 打印结果
解释一下:
- fetch('URL') 是下单,告诉浏览器「去拿这个地址的数据」
- .then(response => ...) 是「外卖到了,先验货」
- .then(data => ...) 是「验货没问题,签收使用」
什么是 async/await?—— 连锁店店长的比喻
上面那种 .then().then() 写多了会很乱,就像你跟外卖小哥说「先拿外卖,再拿饮料,再拿甜点」,结果代码变成:
fetch('https://api.example.com/weather?city=北京')
.then(response => response.json())
.then(data => {
console.log(data);
fetch('https://api.example.com/weather?city=上海')
.then(response => response.json())
.then(data2 => console.log(data2));
});
嵌套越来越多,这就是「回调地狱」。
async/await 就是连锁店店长的办事方式——你跟店长说「帮我搞定北京和上海的天气」,店长说「好的」然后派两个人同时去办,办完了统一汇报。
async function getWeather(city) {
const response = await fetch(`https://api.example.com/weather?city=${city}`);
const data = await response.json();
return data;
}
// 店长一次性派两个任务
async function main() {
const beijing = await getWeather('北京');
const shanghai = await getWeather('上海');
console.log('北京:', beijing);
console.log('上海:', shanghai);
}
这样代码看起来就像普通顺序执行的代码,但实际上是异步的。
什么是 localStorage?—— 手机的备忘录比喻
你用天气 App 查了北京天气,关掉浏览器,明天再打开,数据没了——得重新查。
localStorage 就是浏览器的备忘录。你在手机备忘录里写「明早 8 点开会」,就算手机关机了,明天打开还在。
// 存数据:把天气存进备忘录
localStorage.setItem('weather_beijing', JSON.stringify({
city: '北京',
temp: 26,
condition: '晴'
}));
// 取数据:从备忘录里拿出来用
const saved = localStorage.getItem('weather_beijing');
const weather = JSON.parse(saved);
console.log(weather.city, weather.temp);
解释:
- localStorage.setItem(键, 值) 是「写入备忘录」
- localStorage.getItem(键) 是「读取备忘录」
- 数据必须是字符串,所以对象要用 JSON.stringify() 转一下
什么是 DOM 操作?—— 装修队的比喻
你的 HTML 是个「毛坯房」,里面的 <div> 是空房间。DOM 操作就是装修队——可以往房间里放家具(创建元素)、换墙纸(修改样式)、拆墙(删除元素)。
// 查天气:找到 id="weather"的房间,往里面塞内容
const weatherDiv = document.getElementById('weather');
weatherDiv.innerHTML = `<p>城市:北京</p><p>温度:26°C</p>`;
// 删内容:把这个房间清空
weatherDiv.innerHTML = '';
什么是事件监听?—— 门铃的比喻
用户点了搜索按钮,你得知道「他点了」才能去查天气。
事件监听就是门铃——你把门铃(事件监听器)安装在按钮上,有人按了(触发事件),门铃响了你就去开门(执行回调函数)。
// 给搜索按钮装上门铃
document.getElementById('searchBtn').addEventListener('click', function() {
const city = document.getElementById('cityInput').value;
console.log('用户要查:', city);
// 然后去查天气...
});
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):用 fetch 拿天气数据
先写一个最基础的:用免费天气 API 查天气并打印出来。
这里我们用 wttr.in——一个不需要 API Key 的免费天气服务,拿来练手最方便。
// 项目 1:查天气(基础版)
async function getWeather(city) {
const url = `https://wttr.in/${city}?format=j1`;
const response = await fetch(url);
const data = await response.json();
// wttr.in 返回的数据结构里,当前天气在 current_condition[0]
const current = data.current_condition[0];
console.log(`=== ${city} 天气 ===`);
console.log(`温度:${current.temp_C}°C`);
console.log(`天气:${current.weatherDesc[0].value}`);
console.log(`湿度:${current.humidity}%`);
}
getWeather('Beijing');
预期输出:
=== Beijing 天气 ===
温度:26°C
天气:Partly Cloudy
湿度:65%
解释一下:这代码干了三件事——拼 URL、发起请求、解析并打印结果。就这么简单。
项目 2(15 分钟):完整天气查询页面
现在把项目 1 做成一个完整的 HTML 页面,有输入框、搜索按钮、结果显示区。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>天气查询小工具</title>
<style>
body { font-family: Arial, sans-serif; max-width: 400px; margin: 50px auto; padding: 20px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-top: 20px; }
.weather-item { margin: 10px 0; }
.label { color: #666; }
</style>
</head>
<body>
<h1>🌤️ 天气查询</h1>
<input type="text" id="cityInput" placeholder="输入城市名(英文)" value="Beijing">
<button id="searchBtn">查询</button>
<div id="weatherResult" class="card" style="display:none;"></div>
<script>
async function getWeather(city) {
const url = `https://wttr.in/${city}?format=j1`;
const response = await fetch(url);
const data = await response.json();
const current = data.current_condition[0];
return {
temp: current.temp_C + '°C',
condition: current.weatherDesc[0].value,
humidity: current.humidity + '%'
};
}
function displayWeather(city, weather) {
const resultDiv = document.getElementById('weatherResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h3>📍 ${city}</h3>
<div class="weather-item"><span class="label">温度:</span>${weather.temp}</div>
<div class="weather-item"><span class="label">天气:</span>${weather.condition}</div>
<div class="weather-item"><span class="label">湿度:</span>${weather.humidity}</div>
`;
}
// 给按钮装上门铃
document.getElementById('searchBtn').addEventListener('click', async function() {
const city = document.getElementById('cityInput').value.trim();
if (!city) {
alert('请输入城市名');
return;
}
try {
const weather = await getWeather(city);
displayWeather(city, weather);
} catch (error) {
alert('查询失败,请检查城市名是否正确(需要英文名)');
console.error(error);
}
});
</script>
</body>
</html>
预期输出(输入 "Shanghai" 点击查询后):
📍 Shanghai
温度:28°C
天气:Light Rain
湿度:78%
解释一下:用户输入城市 → 点击按钮 → fetch 去拿数据 → 拿到后用 DOM 操作把结果显示在 div 里。
项目 3(15 分钟):加 localStorage 缓存,做成「历史记录版」
项目 2 的问题是:每次查询都要联网,断网就废了。而且查过的城市下次还得重新输入。
改进方案:查完存到 localStorage,下次打开页面直接显示「上次查过的城市」。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>天气查询(缓存版)</title>
<style>
body { font-family: Arial, sans-serif; max-width: 500px; margin: 50px auto; padding: 20px; }
.card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; margin-top: 20px; }
.weather-item { margin: 10px 0; }
.label { color: #666; }
.history { margin-top: 20px; }
.history-item { padding: 8px; background: #f5f5f5; margin: 5px 0; border-radius: 4px; cursor: pointer; }
.history-item:hover { background: #eee; }
.delete-btn { float: right; color: red; cursor: pointer; }
</style>
</head>
<body>
<h1>🌤️ 天气查询(带历史记录)</h1>
<input type="text" id="cityInput" placeholder="输入城市名(英文)" value="Beijing">
<button id="searchBtn">查询</button>
<div id="weatherResult" class="card" style="display:none;"></div>
<div class="history" id="historyContainer">
<h4>📜 查询历史</h4>
<div id="historyList"></div>
</div>
<script>
// 从 localStorage 读取历史记录
function getHistory() {
const data = localStorage.getItem('weatherHistory');
return data ? JSON.parse(data) : [];
}
// 保存到 localStorage
function saveHistory(city, weather) {
const history = getHistory();
// 避免重复
const exists = history.find(item => item.city.toLowerCase() === city.toLowerCase());
if (exists) return;
history.unshift({ city, weather, time: new Date().toLocaleString() });
// 只保留最近 5 条
if (history.length > 5) history.pop();
localStorage.setItem('weatherHistory', JSON.stringify(history));
renderHistory();
}
// 渲染历史记录列表
function renderHistory() {
const history = getHistory();
const listDiv = document.getElementById('historyList');
listDiv.innerHTML = history.map((item, index) => `
<div class="history-item" onclick="loadFromHistory(${index})">
<span class="delete-btn" onclick="deleteHistory(${index})">✕</span>
<strong>${item.city}</strong> - ${item.weather.temp} ${item.weather.condition}
<br><small>${item.time}</small>
</div>
`).join('');
}
async function getWeather(city) {
const url = `https://wttr.in/${city}?format=j1`;
const response = await fetch(url);
const data = await response.json();
const current = data.current_condition[0];
return {
temp: current.temp_C + '°C',
condition: current.weatherDesc[0].value,
humidity: current.humidity + '%'
};
}
function displayWeather(city, weather) {
const resultDiv = document.getElementById('weatherResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<h3>📍 ${city}</h3>
<div class="weather-item"><span class="label">温度:</span>${weather.temp}</div>
<div class="weather-item"><span class="label">天气:</span>${weather.condition}</div>
<div class="weather-item"><span class="label">湿度:</span>${weather.humidity}</div>
`;
}
function loadFromHistory(index) {
const history = getHistory();
const item = history[index];
document.getElementById('cityInput').value = item.city;
displayWeather(item.city, item.weather);
}
function deleteHistory(index) {
event.stopPropagation();
const history = getHistory();
history.splice(index, 1);
localStorage.setItem('weatherHistory', JSON.stringify(history));
renderHistory();
}
document.getElementById('searchBtn').addEventListener('click', async function() {
const city = document.getElementById('cityInput').value.trim();
if (!city) {
alert('请输入城市名');
return;
}
try {
const weather = await getWeather(city);
displayWeather(city, weather);
saveHistory(city, weather);
} catch (error) {
alert('查询失败,请检查城市名是否正确(需要英文名)');
console.error(error);
}
});
// 页面加载时渲染历史记录
renderHistory();
</script>
</body>
</html>
预期输出:
- 初次使用:空的历史记录列表
- 查询 "Tokyo" 后:历史记录出现 Tokyo,点击可重新加载
- 刷新页面:历史记录依然在(因为存进了 localStorage)
解释一下:这个版本多了三个能力:1)查完自动存历史、2)刷新页面历史不丢、3)点历史记录可以快速重新加载。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:fetch 请求跨域失败
❌ 错误写法:
// 本地直接打开 HTML 文件时,fetch 跨域会失败
fetch('https://api.example.com/data');
✅ 正确做法:
用 wttr.in 这种支持 CORS 的公共 API,或者用 JSONP、或者搭个简单的后端做代理。
坑 2:async 函数没有 await 就调用
❌ 错误写法:
async function getData() { ... }
const result = getData(); // 这是一个 Promise,不是数据!
console.log(result.temp); // undefined
✅ 正确做法:
const result = await getData();
console.log(result.temp); // 正常
坑 3:localStorage 存了非字符串数据
❌ 错误写法:
localStorage.setItem('user', { name: '小明', age: 18 }); // 对象会变成 [object Object]
✅ 正确做法:
localStorage.setItem('user', JSON.stringify({ name: '小明', age: 18 }));
const user = JSON.parse(localStorage.getItem('user'));
坑 4:城市名没做空值校验
❌ 错误写法:
document.getElementById('searchBtn').addEventListener('click', async function() {
const weather = await getWeather(document.getElementById('cityInput').value);
// 用户点搜索时没输入东西,直接请求,会浪费资源
});
✅ 正确做法:
先判断是否有值,没值就弹提示、return 掉(项目 2 里已经这样做了)。
坑 5:fetch 不支持 POST JSON(容易混淆)
❌ 以为这样能发 JSON:
fetch(url, {
method: 'POST',
body: { name: '小明' } // 这不是字符串!
});
✅ 正确做法:
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '小明' })
});
性能小贴士:加个 loading 状态防重复点击
用户手快连点两次「查询」,会发出两个请求,白浪费资源。
let isLoading = false;
document.getElementById('searchBtn').addEventListener('click', async function() {
if (isLoading) return;
isLoading = true;
this.textContent = '查询中...';
try {
const weather = await getWeather(city);
displayWeather(city, weather);
} finally {
isLoading = false;
this.textContent = '查询';
}
});
调试技巧:用 console.log 配合断点
复杂的数据结构看不懂?先打出来看看:
const data = await response.json();
console.log('原始数据:', data); // 展开看结构
console.log('天气描述:', data.current_condition[0].weatherDesc[0].value);
如果代码执行到某一步没反应,在怀疑的位置加个 console.log('走到这里了'),能快速定位卡在哪一步。
✏️ 练习题
练习 1(2 分钟):改个城市名
- 输入:把项目 1 里的
getWeather('Beijing')改成查「东京」 - 预期输出:显示东京的天气
- 提示:wttr.in 支持中文城市名,但用英文 "Tokyo" 更稳
练习 2(2 分钟):加个条件判断
- 输入:在项目 2 的代码里,加一个判断——如果温度低于 10°C,显示「❄️ 有点冷,多穿点」
- 预期输出:查漠河的天气时出现这句话
- 提示:温度在
weather.temp里,是字符串如 "26°C",要转成数字比较
练习 3(3 分钟):用新 API 查空气质量
- 输入:wttr.in 不提供空气质量,换用
aqicn.org的 API(需要申请 token,免费额度够用)或找个免费不需要 key 的替代品 - 预期输出:在项目 2 基础上多显示一个「AQI 空气质量」指标
- 提示:思路和天气查询一样——拼 URL → fetch → 解析 → 显示
练习 4(5 分钟):串起两个项目
- 输入:把项目 2 的查询功能和项目 3 的历史记录功能拆成两个 JS 函数,在项目 3 的搜索按钮事件里同时调用
- 预期输出:功能完整,但代码结构更清晰
- 提示:其实就是把「查询并显示」封装成一个函数,把「存历史」单独一个函数
练习 5(5 分钟):读懂报错
- 输入:下面这段代码运行后报什么错?为什么?
localStorage.setItem('data', { x: 1, y: 2 });
console.log(localStorage.getItem('data').x);
- 预期输出:控制台显示 undefined
- 提示:
localStorage只能存字符串,对象要转 JSON
作业:做一个「天气查询 SPA 增强版」
需求描述:
把项目 3 做成一个真正能用的天气小工具,支持多城市收藏、删除、自动刷新。
功能点:
1. 用户可以「收藏」当前查询的城市(和历史记录不同,收藏是用户主动标记的)
2. 收藏的城市列表要持久化到 localStorage
3. 支持删除单个收藏
4. 刷新页面后,收藏列表依然在
加分项(选做):
1. 给收藏的城市加个「定时刷新」功能,每 5 分钟自动更新一次天气
2. 用 CSS 做个简单的天气图标(比如太阳 ☀️、雨天 🌧️)
验收标准:
- 能跑起来(直接浏览器打开 HTML 文件)
- 输入城市名能显示天气
- 刷新页面后收藏的城市还在
- 代码有适量注释
提交方式:
把完整 HTML 代码发到评论区,或者传 GitHub 贴链接。
📚 总结 + 资源
本文学了 3 个核心技能:
- fetch + async/await —— 异步请求数据,像点外卖一样简单
- localStorage —— 浏览器本地存储,像备忘录一样持久
- DOM 操作 —— 动态操作页面元素,像装修队一样随心改
延伸学习资源:
- MDN Fetch API 文档 —— 权威但稍硬核,当字典查
- wttr.in 官方文档 —— 支持多种格式(JSON/PNG),还能查天气预报
- 《JavaScript 高级程序设计》第 13 章 —— 完整讲了 DOM 和事件模型
互动钩子:
你做天气查询的时候,遇到过什么奇怪的 API 返回格式?或者有什么想加的酷炫功能?评论区聊聊,老粉优先回复!
预告:学会了异步请求和数据存储,下一章我们要玩点不一样的——函数式编程。什么叫纯函数?什么叫高阶函数?为什么 Redux 这么爱用函数式?下一章揭晓。

评论(0)