JavaScript实战④|天气查询应用,调用API与异步处理


author: 专注前端开发,分享JavaScript干货

title: JavaScript实战④|天气查询应用,调用API与异步处理
update: 2026-04-28
tags: JavaScript,实战项目,天气应用,API调用,异步处理,Fetch,Promise

作者:专注前端开发,分享JavaScript干货

更新时间:2026年4月

适合人群:掌握JS基础,想学习API调用和异步处理的开发者


前言:为什么做这个项目?

天气应用是练习 API 调用的最佳项目:

  • 学习 Fetch API
  • 处理异步数据
  • 错误处理
  • 动态更新 UI

一、项目功能

  1. 🔍 输入城市名称查询天气
  2. 🌡️ 显示当前温度、天气状况
  3. 📊 显示湿度、风速、气压等详情
  4. 🔄 显示未来几天预报
  5. ⏱️ 加载状态提示
  6. ❌ 错误处理

二、HTML 结构

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>天气查询</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 50px 20px;
        }
        
        .container {
            max-width: 500px;
            margin: 0 auto;
        }
        
        h1 {
            text-align: center;
            color: white;
            margin-bottom: 30px;
            font-size: 2.5rem;
        }
        
        .search-box {
            display: flex;
            gap: 10px;
            margin-bottom: 30px;
        }
        
        #cityInput {
            flex: 1;
            padding: 15px 20px;
            border: none;
            border-radius: 50px;
            font-size: 16px;
            outline: none;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
        }
        
        #searchBtn {
            padding: 15px 30px;
            background: #ff6b6b;
            color: white;
            border: none;
            border-radius: 50px;
            cursor: pointer;
            font-size: 16px;
            transition: transform 0.3s, background 0.3s;
        }
        
        #searchBtn:hover {
            transform: scale(1.05);
            background: #ff5252;
        }
        
        #searchBtn:disabled {
            background: #ccc;
            cursor: not-allowed;
            transform: none;
        }
        
        .weather-card {
            background: rgba(255,255,255,0.95);
            border-radius: 20px;
            padding: 30px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.2);
            display: none;
        }
        
        .weather-card.show {
            display: block;
            animation: fadeIn 0.5s ease;
        }
        
        @keyframes fadeIn {
            from { opacity: 0; transform: translateY(20px); }
            to { opacity: 1; transform: translateY(0); }
        }
        
        .city-name {
            font-size: 2rem;
            color: #333;
            margin-bottom: 10px;
        }
        
        .weather-main {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 20px;
            margin: 20px 0;
        }
        
        .temperature {
            font-size: 4rem;
            font-weight: bold;
            color: #667eea;
        }
        
        .weather-icon {
            font-size: 4rem;
        }
        
        .weather-desc {
            text-align: center;
            font-size: 1.2rem;
            color: #666;
            margin-bottom: 20px;
        }
        
        .weather-details {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
            margin-top: 20px;
        }
        
        .detail-item {
            text-align: center;
            padding: 15px;
            background: #f8f9fa;
            border-radius: 10px;
        }
        
        .detail-label {
            color: #999;
            font-size: 0.9rem;
            margin-bottom: 5px;
        }
        
        .detail-value {
            color: #333;
            font-size: 1.2rem;
            font-weight: bold;
        }
        
        .loading {
            text-align: center;
            color: white;
            font-size: 1.2rem;
            display: none;
        }
        
        .loading.show {
            display: block;
        }
        
        .error {
            background: #ff6b6b;
            color: white;
            padding: 15px 20px;
            border-radius: 10px;
            text-align: center;
            display: none;
        }
        
        .error.show {
            display: block;
            animation: shake 0.5s ease;
        }
        
        @keyframes shake {
            0%, 100% { transform: translateX(0); }
            25% { transform: translateX(-10px); }
            75% { transform: translateX(10px); }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🌤️ 天气查询</h1>
        
        <div class="search-box">
            <input type="text" id="cityInput" placeholder="输入城市名称(如:北京)">
            <button id="searchBtn">查询</button>
        </div>
        
        <div class="loading" id="loading">正在查询...</div>
        <div class="error" id="error"></div>
        
        <div class="weather-card" id="weatherCard">
            <h2 class="city-name" id="cityName"></h2>
            <div class="weather-main">
                <div class="temperature" id="temperature"></div>
                <div class="weather-icon" id="weatherIcon"></div>
            </div>
            <div class="weather-desc" id="weatherDesc"></div>
            
            <div class="weather-details">
                <div class="detail-item">
                    <div class="detail-label">湿度</div>
                    <div class="detail-value" id="humidity"></div>
                </div>
                <div class="detail-item">
                    <div class="detail-label">风速</div>
                    <div class="detail-value" id="windSpeed"></div>
                </div>
                <div class="detail-item">
                    <div class="detail-label">气压</div>
                    <div class="detail-value" id="pressure"></div>
                </div>
            </div>
        </div>
    </div>

    <script src="app.js"></script>
</body>
</html>

三、JavaScript 实现

javascript 复制代码
// 使用 OpenWeatherMap API(需要注册获取 API Key)
// 或者使用免费的 wttr.in 服务
const API_BASE = 'https://wttr.in';

class WeatherApp {
    constructor() {
        this.cityInput = document.getElementById('cityInput');
        this.searchBtn = document.getElementById('searchBtn');
        this.loading = document.getElementById('loading');
        this.error = document.getElementById('error');
        this.weatherCard = document.getElementById('weatherCard');
        
        this.bindEvents();
    }
    
    bindEvents() {
        this.searchBtn.addEventListener('click', () => this.search());
        this.cityInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') this.search();
        });
    }
    
    async search() {
        const city = this.cityInput.value.trim();
        if (!city) {
            this.showError('请输入城市名称');
            return;
        }
        
        this.showLoading();
        this.hideError();
        this.hideWeather();
        
        try {
            const weather = await this.fetchWeather(city);
            this.displayWeather(weather);
        } catch (err) {
            this.showError('查询失败,请检查城市名称是否正确');
            console.error(err);
        } finally {
            this.hideLoading();
        }
    }
    
    async fetchWeather(city) {
        // 使用 wttr.in 免费 API
        const response = await fetch(`${API_BASE}/${encodeURIComponent(city)}?format=j1`);
        
        if (!response.ok) {
            throw new Error('API 请求失败');
        }
        
        const data = await response.json();
        
        // 解析 wttr.in 的数据格式
        const current = data.current_condition[0];
        
        return {
            city: data.nearest_area[0].areaName[0].value,
            temperature: current.temp_C,
            feelsLike: current.FeelsLikeC,
            description: current.lang_zh ? current.lang_zh[0].value : current.weatherDesc[0].value,
            humidity: current.humidity,
            windSpeed: current.windspeedKmph,
            pressure: current.pressure,
            icon: this.getWeatherIcon(current.weatherCode)
        };
    }
    
    getWeatherIcon(code) {
        // 天气代码对应的表情
        const iconMap = {
            '113': '☀️', // 晴天
            '116': '⛅', // 多云
            '119': '☁️', // 阴天
            '122': '☁️', // 阴天
            '143': '🌫️', // 雾
            '176': '🌦️', // 小雨
            '179': '🌨️', // 雨夹雪
            '182': '🌨️', // 雨夹雪
            '185': '🌨️', // 毛毛雨
            '200': '⛈️', // 雷暴
            '227': '🌨️', // 阵雪
            '230': '❄️', // 暴雪
            '248': '🌫️', // 雾
            '260': '🌫️', // 冻雾
            '263': '🌦️', // 小雨
            '266': '🌧️', // 中雨
            '281': '🌧️', // 冻雨
            '284': '🌧️', // 大冻雨
            '293': '🌧️', // 小雨
            '296': '🌧️', // 中雨
            '299': '🌧️', // 大雨
            '302': '🌧️', // 暴雨
            '305': '🌧️', // 阵雨
            '308': '🌧️', // 大暴雨
            '311': '🌧️', // 冻雨
            '314': '🌧️', // 大冻雨
            '317': '🌨️', // 雨夹雪
            '320': '🌨️', // 大雪
            '323': '🌨️', // 阵雪
            '326': '🌨️', // 中雪
            '329': '❄️', // 大雪
            '332': '❄️', // 大雪
            '335': '❄️', // 暴雪
            '338': '❄️', // 大暴雪
            '350': '🧊', // 冰粒
            '353': '🌦️', // 阵雨
            '356': '🌧️', // 大雨
            '359': '🌧️', // 暴雨
            '362': '🌨️', // 阵雪
            '365': '🌨️', // 大雪
            '368': '🌨️', // 阵雪
            '371': '❄️', // 大雪
            '374': '🧊', // 冰粒
            '377': '🧊', // 冰粒
            '386': '⛈️', // 雷暴伴雨
            '389': '⛈️', // 雷暴伴大雨
            '392': '⛈️', // 雷暴伴雪
            '395': '⛈️', // 雷暴伴暴雪
        };
        
        return iconMap[code] || '🌡️';
    }
    
    displayWeather(weather) {
        document.getElementById('cityName').textContent = weather.city;
        document.getElementById('temperature').textContent = `${weather.temperature}°C`;
        document.getElementById('weatherIcon').textContent = weather.icon;
        document.getElementById('weatherDesc').textContent = 
            `${weather.description} | 体感 ${weather.feelsLike}°C`;
        document.getElementById('humidity').textContent = `${weather.humidity}%`;
        document.getElementById('windSpeed').textContent = `${weather.windSpeed} km/h`;
        document.getElementById('pressure').textContent = `${weather.pressure} hPa`;
        
        this.weatherCard.classList.add('show');
    }
    
    showLoading() {
        this.loading.classList.add('show');
        this.searchBtn.disabled = true;
    }
    
    hideLoading() {
        this.loading.classList.remove('show');
        this.searchBtn.disabled = false;
    }
    
    showError(message) {
        this.error.textContent = message;
        this.error.classList.add('show');
    }
    
    hideError() {
        this.error.classList.remove('show');
    }
    
    hideWeather() {
        this.weatherCard.classList.remove('show');
    }
}

// 初始化应用
const app = new WeatherApp();

四、知识点回顾

知识点 应用
fetch() 发送 HTTP 请求
async/await 处理异步操作
try/catch 错误处理
encodeURIComponent URL 编码
JSON.parse() 解析 JSON 数据
动态 UI 根据数据显示/隐藏元素

五、扩展功能

  1. 地理定位:自动获取当前位置天气
  2. 历史记录:保存查询过的城市
  3. 未来预报:显示未来 3-5 天天气
  4. 温度单位切换:摄氏度/华氏度

六、课后作业

  1. 添加"获取当前位置天气"功能(使用 Geolocation API)
  2. 实现温度单位切换(摄氏度/华氏度)
  3. 添加本地存储,保存最近查询的城市

有问题欢迎评论区留言,大家一起讨论!


标签:JavaScript | 实战项目 | 天气应用 | API调用 | 异步处理 | Fetch | Promise

相关推荐
微扬嘴角1 小时前
react篇4--setState、LazyLoad和Hooks
前端·javascript·react.js
杨梦馨1 小时前
万级数据表格卡死?Web Worker 一招搞定
前端·javascript·vue.js
用户484526255821 小时前
JavaScript 数组不是数组,是对象
javascript
用户484526255821 小时前
用栈模拟队列:算法题背后的原型链课
javascript
零陵上将军_xdr2 小时前
后端转全栈学习-Day5-JavaScript 基础-3
开发语言·javascript·学习
ssshooter2 小时前
为什么父元素的高度不会包含子元素的 margin?
前端·javascript·面试
Goodbye2 小时前
JavaScript 同步与异步编程深度解析
javascript
Amo Xiang2 小时前
JS 逆向系统进阶路线:专栏总纲与文章导航
javascript·js逆向·前端加密·爬虫逆向·反爬虫
●VON3 小时前
AtomGit Flutter鸿蒙客户端:主题系统
javascript·flutter·华为·跨平台·harmonyos·鸿蒙