通过网盘分享的文件:天气查询小应用.zip
链接: https://pan.baidu.com/s/14KmANjtUA_1qqwbROLC7iA?pwd=ynaq 提取码: ynaq
–来自百度网盘超级会员v7的分享
用 HTML + CSS + 原生 JavaScript 做了一个天气查询小应用,没有引入任何框架、构建工具和第三方库,双击index.html(或本地起个静态服务器)就能跑。
数据源用的是Open-Meteo:完全免费、不需要 API Key、原生开启 CORS,浏览器可以直接请求。
一、功能一览
1. 按城市名查询当前天气
输入城市名(支持中文、英文、拼音),展示:温度、天气状况、体感温度、湿度、风速。
2. 未来 7 天预报
逐日预报以卡片形式横向平铺,每张卡片包含日期(今天 / 明天 / 10/06 周一)、天气图标、天气状况、最高 / 最低温。
3. 按天气切换背景与图标
晴天是暖橙渐变、阴天是灰蓝渐变、雨天是深蓝渐变、雪天是浅蓝渐变。整页背景平滑过渡,预报卡片也会按各自的天气做轻微着色,一眼就能区分「晴 / 阴 / 雨」。
4. 加载中与失败重试
请求期间显示旋转 loading;请求失败展示友好中文提示(超时 / 断网 / 没找到城市 / 跨域被拦截),并附带「重试」按钮,重试会复用上一次的查询任务,不需要重新输入。
5. 最近查询记录
查过的城市自动存入localStorage,去重后置顶、最多保留 6 条,点击标签即可重新查询。
6. 定位当前城市
点击 📍 调用浏览器 Geolocation API 获取坐标,再反向解析出城市名并查询天气。
二、技术点
1. 零依赖,三件套直接跑
没有 npm、没有打包、没有框架。index.html+style.css+app.js三个文件,用浏览器打开就能运行,非常适合作为前端基础练习。
2. 全面 async / await,告别回调地狱
fetch、Geolocation、localStorage 的读写全部用async/await串联,代码是自上而下的线性结构:
asyncfunctionfetchWeatherByCity(cityName){constgeo=awaitrequest(API.GEOCODE,{params:{name:cityName,...}});if(!geo.results?.length)thrownewError(`没有找到城市「${cityName}」`);constplace=geo.results[0];returnfetchWeatherByCoords(place.latitude,place.longitude,displayName(place,cityName));}Geolocation 本身是回调式 API,用一个 Promise 包一层就能无缝接入await:
functiongetCurrentPosition(){returnnewPromise((resolve,reject)=>{navigator.geolocation.getCurrentPosition((pos)=>resolve(pos.coords),(err)=>reject(newError(messages[err.code])));});}3. 请求统一封装:超时 + 错误归一化
所有网络请求都走同一个request()函数,业务代码里不出现裸fetch。它负责三件事:
- 拼参:用
URLSearchParams自动做 URL 编码,中文城市名不会出错; - 超时:用
AbortController+setTimeout实现,超过 8 秒主动中止请求; - 错误归一化:把 HTTP 4xx/5xx、网络失败、超时这三种情况统一翻译成中文友好提示。
constcontroller=newAbortController();consttimer=setTimeout(()=>controller.abort(),timeout);try{constres=awaitfetch(fullUrl,{signal:controller.signal});if(!res.ok)thrownewError(`服务器返回错误(HTTP${res.status})`);returnawaitres.json();}catch(err){if(err.name==="AbortError")thrownewError("请求超时,请重试");thrownewError("网络请求失败:可能断网或跨域被拦截,请重试");}finally{clearTimeout(timer);// 别忘了清定时器}一个容易踩的坑:
fetch只在网络层失败时才 reject,HTTP 404 / 500 它是正常 resolve 的,必须手动判断res.ok。
4. CORS:为什么直接请求接口会报跨域
浏览器有同源策略:协议 + 域名 + 端口三者完全相同才算同源。页面在http://localhost:5500,接口在https://api.open-meteo.com,域名不同就是跨域。
关键认知(很多人会搞错):
- 跨域时请求其实已经发出去了,服务器也返回了数据;
- 是浏览器发现响应头里没有
Access-Control-Allow-Origin,才拒绝把响应交给 JS,控制台报blocked by CORS policy; - 所以Postman / curl 不报错,只有浏览器报错—— 同源策略只存在于浏览器。
CORS(跨源资源共享)就是服务器主动授权的机制,在响应头里写上:
Access-Control-Allow-Origin: *等于告诉浏览器「我允许别的站点读我的数据」。
本项目的具体结论:Open-Meteo 返回了Access-Control-Allow-Origin: *,所以浏览器可以直连,不需要任何代理。而国内的多数天气接口(和风、高德、心知等)默认不返回这个头,那时才需要处理:
| 方案 | 说明 |
|---|---|
| 选已开启 CORS 的接口 | 最省事,本项目就是这条 |
本地静态服务器打开(http://) | 不要用file://,它的 origin 是null,更容易被拒 |
CORS 代理(如corsproxy.io) | 仅用于测试,不要上生产 |
| 后端 / Serverless 转发 | 生产级做法,还能顺便藏 API Key |
5. 数据归一化 + WMO 天气码映射
接口原始的current/daily结构比较琐碎,中间加一层normalize()把数据整理成内部统一模型,UI 只认这个结构:
{city:"北京 · 北京市",current:{temp:18,feelsLike:13,humidity:25,windSpeed:4.38,condition:"晴",icon:"☀️",theme:"sunny"},daily:[{date:"2026-10-04",max:21,min:15,condition:"晴",...},...]}好处是:以后换数据源,只改normalize(),UI 一行都不用动。
天气状况用的是国际标准WMO weather_code,维护一张code → 中文描述 + 图标 + 主题名的映射表,未知码做兜底,顺带就解决了「图标 + 主题切换」。
6. 主题切换:CSS 变量 +data-theme
不需要写一堆 class 切换逻辑,只要给<body>换一个data-theme属性,CSS 里对应的背景就会生效,配合transition做出平滑过渡:
body[data-theme="sunny"]{background:linear-gradient(160deg,#f7b733,#e8890c,#b45309);}body[data-theme="rainy"]{background:linear-gradient(160deg,#3b5f83,#1e293b);}body[data-theme="cloudy"]{background:linear-gradient(160deg,#6b7c93,#3b4657);}document.body.dataset.theme=data.current.theme;7. localStorage 存最近查询
写入时先按城市名去重、再unshift置顶、最后slice限量,读取时用try/catch兜底,防止数据被手动改坏导致整页崩溃。
constlist=loadHistory().filter((item)=>item!==city);list.unshift(city);localStorage.setItem(HISTORY_KEY,JSON.stringify(list.slice(0,6)));8. 状态机式的 UI 管理
页面只有四种互斥状态:idle(初始提示)、loading、error、weather。一个show(state)函数统一切换,避免出现「loading 和错误提示同时显示」这类状态错乱。
functionshow(state){Object.entries(STATE_ELEMENTS).forEach(([name,node])=>{node.classList.toggle("hidden",name!==state);});}9. 重试机制
把「上一次做的事」记下来,重试时原样再跑一遍:
letlastTask=null;asyncfunctionrunAction(action,{saveHistory=true}={}){lastTask={action,saveHistory};show("loading");try{constdata=awaitaction();renderWeather(data);applyTheme(data.current.theme);show("weather");}catch(err){el.errorMsg.textContent=err.message;show("error");}}10. 安全与健壮性细节
- 城市名、接口返回的文本一律先转义再拼进
innerHTML,避免 XSS; - 逆地理编码(坐标 → 城市名)失败时不阻断主流程,降级成「当前位置」照常显示天气;
- 定位功能的失败原因(拒绝授权 / 无法获取 / 超时)分别给出不同的中文提示。
11. 用到的接口一览
| 用途 | 接口 |
|---|---|
| 城市名 → 经纬度 | https://geocoding-api.open-meteo.com/v1/search |
| 经纬度 → 当前天气 + 逐日预报 | https://api.open-meteo.com/v1/forecast |
| 经纬度 → 城市名(定位用,第三方) | https://api.bigdatacloud.net/data/reverse-geocode-client |
一次forecast请求就能同时拿到current(当前天气)和daily(逐日预报),参数里指定timezone=auto按目标地点时区返回日期,避免跨天错位。
三、小结
这个项目的复杂度不高,但把前端入门阶段最该练的几件事都串了一遍:异步流程控制、请求封装与容错、跨域原理、数据归一化、状态管理、本地存储、浏览器原生 API。最大的感受是——不用框架,反而更容易看清每一层到底在做什么。