拓冰建站拓冰建站
首页 / 资讯中心 / 正文

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 令牌桶,还是简单的本地内存计数? 欢迎在评论区分享你的实战经验,我们一起交流踩坑心得。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门