3个坑填平,手写实现天气预报模块
3个坑填平,手写实现天气预报模块 学会语法却不知怎么搭项目?这是很多初级开发者的通病。代码能跑,一集成就崩,或者性能差到没法看。今天不整虚的,直接上手手写实现一个完整的天气预报模块。 从数据获取到前端展示,全流程拆解。你会看到如何绕过常见的API陷阱,如何设计可扩展的目录结构,以及如何优化加载体验。 项目目标与核心难点 别急着写代码,先想清楚我们要解决什么。 一个合格的天气预报模块,核心诉求只有三个:准(数据实时)、快(响应迅速)、稳(异常可兜底)。 很多教程只教你怎么调API,却忽略了“稳”。比如,用户所在的城市API突然挂了怎么办?缓存过期了怎么办?网络波动导致JSON解析失败怎么办? 我们的目标很明确:零依赖前端:原生JavaScript + CSS,不引入Vue/React,方便你理解底层逻辑。 后端代理:使用Node.js + Express做中间层,隐藏API Key,处理跨域。 本地缓存:利用LocalStorage或Redis(简化版用Local)降低请求频率,提升加载速度。 容错机制:API失败时,展示离线数据或友好提示,而不是白屏。这个结构虽然简单,但涵盖了真实项目中的90%痛点。 目录结构设计 工欲善其事,必先利其器。混乱的文件结构是维护噩梦。 建议采用如下结构,清晰分层: weather-app/ ├── client/ # 前端资源 │ ├── index.html # 入口页面 │ ├── css/ │ │ └── style.css # 样式文件 │ └── js/ │ ├── main.js # 主逻辑 │ ├── api.js # 网络请求封装 │ └── utils.js # 工具函数(格式化时间、单位转换) ├── server/ # 后端服务 │ ├── index.js # 服务器入口 │ ├── config.js # 配置文件(API Key等) │ └── routes/ │ └── weather.js# 路由处理 ├── package.json # 依赖管理 └── README.md为什么要把API Key放在后端? 这是安全底线。如果在前端代码里直接写API Key,任何人都能F12查看并盗用。虽然OpenWeatherMap等部分API支持限制来源IP,但通过后端代理是最稳妥的做法。 关于模块化: api.js 只负责发请求,utils.js 只负责数据加工,main.js 负责UI渲染。这样当你要换数据源(比如从OpenWeatherMap换成和风天气)时,只需要改 api.js,其他代码几乎不用动。 核心代码实现 下面进入硬核部分。我们将分前端和后端两部分讲解。 1. 后端:Node.js 代理服务 创建 server/index.js。这里我们使用 Express 框架,因为它轻量且生态丰富。 const express = require('express'); const http = require('http'); const path = require('path'); const { WEATHER_API_KEY } = require('./config');const app = express(); const server = http.createServer(app);// 1. 设置JSON解析 app.use(express.json());// 2. 静态文件服务,方便开发时直接访问前端 app.use(express.static(path.join(__dirname, '../client')));// 3. 核心接口:/api/weather app.get('/api/weather', async (req, res) = {const { city } = req.query;// 参数校验:防止恶意请求if (!city) {return res.status(400).json({ error: 'City name is required' });}try {// 假设使用 OpenWeatherMap 免费APIconst apiEndpoint = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}appid=${WEATHER_API_KEY}units=metric`;// 使用 fetch (Node 18+ 内置) 或 axiosconst response = await fetch(apiEndpoint);if (!response.ok) {throw new Error(`API Error: ${response.status}`);}const data = await response.json();// 4. 数据预处理:只返回前端需要的字段,减少传输体积const formattedData = {city: data.name,temp: Math.round(data.main.temp),feelsLike: Math.round(data.main.feels_like),humidity: data.main.humidity,description: data.weather[0].description,icon: data.weather[0].icon,windSpeed: data.wind.speed};res.json(formattedData);} catch (error) {console.error('Fetch Error:', error);// 5. 异常捕获:返回统一错误格式res.status(500).json({ error: 'Failed to fetch weather data', message: error.message });} });// 6. 启动服务 const PORT = 3000; server.listen(PORT, () = {console.log(`Server running on http://localhost:${PORT}`); });关键点解析:encodeURIComponent(city):城市名可能包含中文或空格,必须进行URL编码,否则API会报错404。这是新手最常踩的坑之一。 数据裁剪:API返回的JSON可能有几KB,我们只挑温度、湿度、图标等必要字段。这在移动端弱网环境下至关重要。 异常处理:try-catch 包裹整个异步操作。如果API超时或网络断开,前端能收到500状态码,从而展示友好提示,而不是卡死。2. 前端:原生JS请求与渲染 打开 client/js/api.js,封装一个通用的请求函数。 // api.js export const fetchWeather = async (city) = {try {const response = await fetch(`/api/weather?city=${encodeURIComponent(city)}`);if (!response.ok) {throw new Error('Network response was not ok');}return await response.json();} catch (error) {console.error('Fetch failed:', error);// 这里可以抛出特定错误,让 main.js 处理throw error;} };接着是 client/js/main.js,负责UI交互。 // main.js import { fetchWeather } from './api.js';const searchBtn = document.getElementById('search-btn'); const cityInput = document.getElementById('city-input'); const weatherDisplay = document.getElementById('weather-display'); const loadingIndicator = document.getElementById('loading'); const errorMsg = document.getElementById('error-msg');// 1. 事件监听 searchBtn.addEventListener('click', handleSearch); cityInput.addEventListener('keypress', (e) = {if (e.key === 'Enter') handleSearch(); });async function handleSearch() {const city = cityInput.value.trim();// 前端二次校验if (!city) {showError('请输入城市名称');return;}// 2. 状态管理:显示LoadingshowLoading(true);hideError();hideWeather();try {// 3. 发起请求const data = await fetchWeather(city);// 4. 渲染数据renderWeather(data);} catch (error) {// 5. 错误处理console.error('Error:', error);showError('获取天气失败,请稍后重试');} finally {// 6. 无论成功失败,都要关闭LoadingshowLoading(false);} }function renderWeather(data) {const html = `div class=weather-cardh2${data.city}/h2div class=temp${data.temp}°C/divp${data.description}/pdiv class=detailsspan体感: ${data.feelsLike}°C/spanspan湿度: ${data.humidity}%/spanspan风速: ${data.windSpeed} m/s/span/divimg src=https://openweathermap.org/img/wn/${data.icon}@2x.png alt=Weather Icon/div`;weatherDisplay.innerHTML = html;weatherDisplay.style.display = 'block'; }// 辅助函数:Loading, Error, Hide function showLoading(show) {loadingIndicator.style.display = show ? 'block' : 'none'; }function showError(msg) {errorMsg.textContent = msg;errorMsg.style.display = 'block'; }function hideError() {errorMsg.style.display = 'none'; }function hideWeather() {weatherDisplay.style.display = 'none'; }这段代码体现了什么工程思维?状态分离:Loading、Error、Success 是三种互斥状态。finally 块确保 Loading 一定会消失,避免UI卡住。 DOM操作最小化:使用 innerHTML 一次性注入模板,而不是逐个创建节点。虽然不如虚拟DOM高效,但在简单场景下性能足够,且代码可读性更好。 异步/await:避免了回调地狱,逻辑线性清晰。运行与测试 代码写完了,怎么验证?安装依赖: npm install express(注:Node.js 18+ 内置 fetch,无需额外安装 axios 或 node-fetch)启动服务: node server/index.js访问测试: 打开浏览器访问 http://localhost:3000。测试用例:正常场景:输入 Beijing,检查是否显示正确温度和图标。 异常场景1:输入空字符串,点击搜索。应显示“请输入城市名称”。 异常场景2:输入不存在的城市 Xyzabc。API应返回404,后端捕获后返回500,前端显示“获取天气失败”。 网络断开:拔掉网线,点击搜索。fetch 会抛出 TypeError,前端同样捕获并显示错误提示。这里有一个重要的测试技巧:使用浏览器的开发者工具,将网络状态设置为 Slow 3G。你会发现 Loading 动画变得非常重要。如果此时页面没有反馈,用户会疯狂点击按钮,导致并发请求堆积。 优化扩展 基础功能跑通后,怎么让它更像生产级产品? 1. 增加本地缓存(LocalStorage) 用户每次刷新页面都请求API是浪费资源。我们可以这样优化: 在 main.js 的 handleSearch 中: async function handleSearch() {const city = cityInput.value.trim();if (!city) return;// 1. 先查本地缓存const cachedData = localStorage.getItem(`weather_${city}`);if (cachedData) {const parsedData = JSON.parse(cachedData);// 判断缓存是否过期(例如1小时)if (Date.now() - parsedData.timestamp 3600000) {renderWeather(parsedData.data);return;}}// 2. 缓存失效,请求APIshowLoading(true);try {const data = await fetchWeather(city);// 3. 存入缓存localStorage.setItem(`weather_${city}`, JSON.stringify({data: data,timestamp: Date.now()}));renderWeather(data);} catch (error) {// 4. API失败,尝试使用过期缓存(降级策略)if (cachedData) {const parsedData = JSON.parse(cachedData);renderWeather(parsedData.data);showError('数据可能已过期');} else {showError('获取天气失败');}} finally {showLoading(false);} }这个策略叫“Stale-While-Revalidate”。即使网络断了,用户也能看到上一次的数据,体验不会中断。 2. 地理定位(Geolocation) 让用户手动输入城市很麻烦。我们可以自动获取位置。 在 main.js 添加: if (navigator.geolocation) {navigator.geolocation.getCurrentPosition((position) = {// 拿到经纬度后,逆地理编码获取城市名,或者直接用经纬度调API// 这里简化处理,假设我们能用经纬度查天气fetchWeatherByCoords(position.coords.latitude, position.coords.longitude);},(error) = {console.log('定位失败,请手动输入城市');}); }注意:浏览器会弹出权限请求。如果用户拒绝,必须提供手动输入框作为兜底。 3. 防抖搜索 如果用户想实现“输入即搜索”(Input Search),必须加防抖。 function debounce(func, wait) {let timeout;return function executedFunction(...args) {const later = () = {clearTimeout(timeout);func(...args);};clearTimeout(timeout);timeout = setTimeout(later, wait);}; }// 使用 const debouncedSearch = debounce(handleSearch, 500); cityInput.addEventListener('input', debouncedSearch);这能避免用户每敲一个字母就发一次请求,极大节省API配额。 小结与避坑指南 回顾一下,我们从零搭建了一个手写实现的天气预报模块。 常见避坑清单:跨域问题:永远不要在前端直接调第三方API,一定要过一遍自己的后端。 API Key泄露:前端代码是公开的,Key放在后端配置文件中。 单位混淆:OpenWeatherMap 默认单位是开尔文(K),记得加 units=metric 转成摄氏度(C),或者在前端做 K - 273.15 转换。 时区问题:API返回的时间戳通常是UTC时间。展示给中国用户时,需要转换为本地时区(new Date(timestamp * 1000).toLocaleString('zh-CN'))。这个模块的价值: 它不仅仅是一个天气查询工具,更是一个微服务架构的雏形。你可以把天气模块封装成一个独立的Service,通过RESTful API暴露给其他项目调用。 在实际工作中,这种“小而美”的模块往往比大而全的系统更容易落地和复用。 最后,抛出一个问题给你: 你公司项目里是怎么处理第三方API依赖的?是直接前端调用,还是统一走网关代理?对于API限流(Rate Limiting),你们是用 Redis 令牌桶,还是简单的本地内存计数? 欢迎在评论区分享你的实战经验,我们一起交流踩坑心得。

相关新闻

骁龙450避坑指南:3个致命错误与完整示例解析

骁龙450避坑指南:3个致命错误与完整示例解析

骁龙450避坑指南:3个致命错误与完整示例解析 刚学完Java基础,对着文档敲了一堆Hello World,结果一到实际项目就抓瞎?别慌,我当年也这样。很多人卡在“语法会写,项目不会搭”的泥潭里,尤其是处理像骁龙450这类嵌入式或IoT场景…

2026/9/22 5:44:43 阅读更多 →
3步搞定有限理性决策模型,一文搞懂代码实战

3步搞定有限理性决策模型,一文搞懂代码实战

3步搞定有限理性决策模型,一文搞懂代码实战 版本升级后 API 全变了,是不是让你抓狂?别慌,今天咱们不聊虚的,直接上硬货。很多后端和算法工程师在重构推荐系统或风控引擎时,发现原有的全理性假设模型在复杂场景下失效,这时候 有限理性…

2026/9/22 5:44:43 阅读更多 →
3大主流方案对比:wownei实战完整示例与选型避坑指南

3大主流方案对比:wownei实战完整示例与选型避坑指南

3大主流方案对比:wownei实战完整示例与选型避坑指南 面对满屏红色的报错日志和深不见底的 StackTrace,你是不是也感到一阵窒息?别慌,这种“看不懂、改不动、复现难”的状态,正是从新手迈向资深工程师的必经阵痛。今天不整虚的,直接上…

2026/9/22 5:44:43 阅读更多 →

最新新闻

3步搞懂一键gost源码,面试必问的底层逻辑

3步搞懂一键gost源码,面试必问的底层逻辑

3步搞懂一键gost源码,面试必问的底层逻辑 官方文档那几百页的 PDF 和晦涩的 Wiki,看完脑子还是一团浆糊?别急,这不仅是你的问题,也是很多资深开发者的常态。尤其是面对 一键gost…

2026/9/22 6:26:10 阅读更多 →
3个报错教你搞懂月光墨鱼完整示例

3个报错教你搞懂月光墨鱼完整示例

3个报错教你搞懂月光墨鱼完整示例 半夜三点,IDE 屏幕上一片红色。 NullPointerException 、 StackOverflowError 混着 IllegalStateException ,StackTrace…

2026/9/22 6:26:10 阅读更多 →
3个KFB实战技巧助你从入门到精通告别低效

3个KFB实战技巧助你从入门到精通告别低效

3个KFB实战技巧助你从入门到精通告别低效 刚啃完KFB文档,对着代码发呆?别慌,这是90%新手的通病。你会写语法,但不知道项目里怎么用,导致性能一上量就崩。从入门到精通,关键不在背API,而在懂业务场景下的性能优化。 KFB(Kafka…

2026/9/22 6:26:10 阅读更多 →
迷你酷狗播放器实战:3个API坑让新手避坑指南

迷你酷狗播放器实战:3个API坑让新手避坑指南

迷你酷狗播放器实战:3个API坑让新手避坑指南 版本升级后 API 全变了,这是无数做桌面端二次开发的新手在接手酷狗音乐旧项目时的噩梦。你满心欢喜地打开 GitHub…

2026/9/22 6:26:10 阅读更多 →
2026最新java手机游戏模拟器面试必问:API变更与报错解决

2026最新java手机游戏模拟器面试必问:API变更与报错解决

2026最新java手机游戏模拟器面试必问:API变更与报错解决 版本升级后 API 全变了?别慌,这正是2026最新java手机游戏模拟器面试的“照妖镜”。…

2026/9/22 6:25:09 阅读更多 →
3个关键步骤搞定对接工作,源码解析揭秘API变动真相

3个关键步骤搞定对接工作,源码解析揭秘API变动真相

3个关键步骤搞定对接工作,源码解析揭秘API变动真相 版本升级后 API 全变了,这是无数开发者在项目中遇到的噩梦。刚部署好的服务,一升级依赖库或中间件,接口调用直接报错,调试时间比写业务逻辑还长。很多人只盯着报错日志改代码,却忽略了背后的…

2026/9/22 6:25:09 阅读更多 →

日新闻

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天

3台商务办公笔记本实测:手写实现环境配置,告别卡半天 配置环境就卡半天?别怪机器慢,多半是你没选对工具链。在Java、Go或Python的项目现场, 手写实现…

2026/9/22 0:00:41 阅读更多 →
剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑

剑帝加点速查手册:3分钟搞懂核心逻辑 面试被问原理答不上来,是不是常态?别慌。很多开发者对着 GitHub 开源仓库里的代码发呆,看似简单实则暗藏玄机。今天这份【剑帝加点】速查手册,直接带你拆解核心实现,把面试必考的原理讲透。…

2026/9/22 0:00:41 阅读更多 →
手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优

手写实现图片压缩网站核心:搞定WebP转换与质量调优 复制来的代码跑不通不知道怎么调?别慌,这种“复制粘贴地狱”在开发圈太常见了。尤其是做 图片压缩网站…

2026/9/22 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/22 4:32:41 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/22 4:38:57 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/21 4:51:05 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/21 15:36:51 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/21 15:36:51 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/22 2:43:42 阅读更多 →