news 2026/9/22 5:45:43

3个坑填平,手写实现天气预报模块

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
3个坑填平,手写实现天气预报模块

3个坑填平,手写实现天气预报模块

学会语法却不知怎么搭项目?这是很多初级开发者的通病。代码能跑,一集成就崩,或者性能差到没法看。今天不整虚的,直接上手手写实现一个完整的天气预报模块。

从数据获取到前端展示,全流程拆解。你会看到如何绕过常见的API陷阱,如何设计可扩展的目录结构,以及如何优化加载体验。

项目目标与核心难点

别急着写代码,先想清楚我们要解决什么。

一个合格的天气预报模块,核心诉求只有三个:(数据实时)、(响应迅速)、(异常可兜底)。

很多教程只教你怎么调API,却忽略了“稳”。比如,用户所在的城市API突然挂了怎么办?缓存过期了怎么办?网络波动导致JSON解析失败怎么办?

我们的目标很明确:

  1. 零依赖前端:原生JavaScript + CSS,不引入Vue/React,方便你理解底层逻辑。
  2. 后端代理:使用Node.js + Express做中间层,隐藏API Key,处理跨域。
  3. 本地缓存:利用LocalStorage或Redis(简化版用Local)降低请求频率,提升加载速度。
  4. 容错机制: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-card"><h2>${data.city}</h2><div class="temp">${data.temp}°C</div><p>${data.description}</p><div class="details"><span>体感: ${data.feelsLike}°C</span><span>湿度: ${data.humidity}%</span><span>风速: ${data.windSpeed} m/s</span></div><img 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';
}

这段代码体现了什么工程思维?

  1. 状态分离:Loading、Error、Success 是三种互斥状态。finally 块确保 Loading 一定会消失,避免UI卡住。
  2. DOM操作最小化:使用 innerHTML 一次性注入模板,而不是逐个创建节点。虽然不如虚拟DOM高效,但在简单场景下性能足够,且代码可读性更好。
  3. 异步/await:避免了回调地狱,逻辑线性清晰。

运行与测试

代码写完了,怎么验证?

  1. 安装依赖

    npm install express
    

    (注:Node.js 18+ 内置 fetch,无需额外安装 axios 或 node-fetch)

  2. 启动服务

    node server/index.js
    
  3. 访问测试: 打开浏览器访问 http://localhost:3000

测试用例:

  • 正常场景:输入 "Beijing",检查是否显示正确温度和图标。
  • 异常场景1:输入空字符串,点击搜索。应显示“请输入城市名称”。
  • 异常场景2:输入不存在的城市 "Xyzabc"。API应返回404,后端捕获后返回500,前端显示“获取天气失败”。
  • 网络断开:拔掉网线,点击搜索。fetch 会抛出 TypeError,前端同样捕获并显示错误提示。

这里有一个重要的测试技巧:使用浏览器的开发者工具,将网络状态设置为 "Slow 3G"。你会发现 Loading 动画变得非常重要。如果此时页面没有反馈,用户会疯狂点击按钮,导致并发请求堆积。

优化扩展

基础功能跑通后,怎么让它更像生产级产品?

1. 增加本地缓存(LocalStorage)

用户每次刷新页面都请求API是浪费资源。我们可以这样优化:

main.jshandleSearch 中:

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配额。

小结与避坑指南

回顾一下,我们从零搭建了一个手写实现的天气预报模块。

常见避坑清单:

  1. 跨域问题:永远不要在前端直接调第三方API,一定要过一遍自己的后端。
  2. API Key泄露:前端代码是公开的,Key放在后端配置文件中。
  3. 单位混淆:OpenWeatherMap 默认单位是开尔文(K),记得加 &units=metric 转成摄氏度(C),或者在前端做 K - 273.15 转换。
  4. 时区问题:API返回的时间戳通常是UTC时间。展示给中国用户时,需要转换为本地时区(new Date(timestamp * 1000).toLocaleString('zh-CN'))。

这个模块的价值:

它不仅仅是一个天气查询工具,更是一个微服务架构的雏形。你可以把天气模块封装成一个独立的Service,通过RESTful API暴露给其他项目调用。

在实际工作中,这种“小而美”的模块往往比大而全的系统更容易落地和复用。

最后,抛出一个问题给你:

你公司项目里是怎么处理第三方API依赖的?是直接前端调用,还是统一走网关代理?对于API限流(Rate Limiting),你们是用 Redis 令牌桶,还是简单的本地内存计数?

欢迎在评论区分享你的实战经验,我们一起交流踩坑心得。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/22 5:45:43

搞定英语四六级单词,这3个性能优化坑让你少写1000行代码

搞定英语四六级单词,这3个性能优化坑让你少写1000行代码 刚毕业那会儿,我盯着满屏的英语四六级单词,脑子里全是 for 循环和 if 判断。语法我会背,但一动手搭项目就卡壳:为什么加载几万条单词数据,页面卡得像PPT?为什么搜索一个词,CPU占用率飙升?…

作者头像 李华
网站建设 2026/9/22 5:45:31

3步搞定impotent性能优化保姆级教程

3步搞定impotent性能优化保姆级教程 看了一堆教程还是不会写项目?别急,这很正常。很多开发者卡在“懂原理”和“能落地”之间,就是因为没搞懂底层那些看似不起眼的细节。今天这篇 保姆级教程 ,不玩虚的,直接拆解 impotent…

作者头像 李华
网站建设 2026/9/22 5:45:28

香港和深圳原理详解

3个坑让接口慢3倍?手写实现优化深圳到香港数据同步 代码复制过来,本地跑通,一上深圳生产环境直接超时。你盯着报错日志发懵,不知道是网络问题、连接池没调,还是代码逻辑本身就有性能黑洞。别慌,这种“看起来对,跑起来崩”的情况,后端开发几乎人人都会遇到。尤其是处理跨地域数据同步(比如深圳总部到香港分公司)…

作者头像 李华
网站建设 2026/9/22 5:45:09

3个面试陷阱:哺乳类动物分类学速查手册

3个面试陷阱:哺乳类动物分类学速查手册 面试被问“哺乳类动物”底层原理答不上来,瞬间脑空白?别慌,这行混久了都知道,很多基础概念看似简单,实则藏着无数坑。手里没份靠谱的 速查手册 ,现场真容易露怯。 考点梳理…

作者头像 李华
网站建设 2026/9/22 5:45:00

信息安全整改方案里的性能坑,3个高频面试题代码拆解

信息安全整改方案里的性能坑,3个高频面试题代码拆解 官方文档动辄几百页,翻到第三页就犯困,关键配置项藏在附录里,改完代码跑测试还是慢,这种抓不住重点的挫败感谁懂? 很多开发者在应对 信息安全整改方案 时,往往把重心全放在了加密算法和权限控制上,却忽略了底层处理逻辑的性能损耗。更扎心的是,…

作者头像 李华
网站建设 2026/9/22 5:44:58

3个免费网站加速避坑指南:小白也能看懂的CDN原理与实战

3个免费网站加速避坑指南:小白也能看懂的CDN原理与实战 复制来的加速代码跑不通?报错一堆不知道咋调?别慌,这确实是很多刚接手项目的管理员最容易踩的坑。今天这篇避坑指南,不讲虚的,直接带你搞懂 免费网站加速 到底在加速什么,为什么有时候加了CDN反而更慢,以及怎么用最少的成本让网站飞起来。…

作者头像 李华