2025/12/25 每日总结 天气预报应用 - OpenWeatherMap

本文以日志形式记录我从零搭建这个天气预报网页的全过程,重点讲解OpenWeatherMap API的配置与实际应用,以及如何结合原生HTML/CSS/JS实现丰富的视觉和交互效果。

一、前期准备

1. 技术栈选型

  • 核心技术:HTML5 + CSS3 + JavaScript(原生开发,无框架依赖,降低学习门槛)
  • 辅助工具库:
    • Chart.js:绘制温度趋势图表,直观展示天气变化规律
    • Font Awesome:提供丰富的天气图标,快速提升视觉效果
  • 开发工具:VS Code(搭配Live Server插件)、Chrome开发者工具

2. API选择:为什么选OpenWeatherMap?

对比多个天气API后,最终选择OpenWeatherMap的原因:

  • 免费额度充足:每分钟60次请求,完全满足个人/测试使用
  • 接口丰富:支持当前天气、7天预报、小时预报、空气质量等全维度数据
  • 文档清晰:参数说明、返回格式一目了然,支持多语言/多单位配置
  • 全球覆盖:支持国内外城市查询,数据更新及时

二、OpenWeatherMap API核心配置

1. 注册与API Key获取

这是调用API的第一步,步骤如下:

  1. 访问OpenWeatherMap官网,点击「Sign Up」完成账号注册
  2. 登录后进入API Keys页面
  3. 系统会默认生成一个API Key,也可点击「Generate」自定义创建
  4. ⚠️ 关键提醒:新生成的API Key需要等待10-20分钟生效,立即调用会提示"invalid API key"

2. 核心接口说明

本次项目主要用到3个核心接口,整理如下:

接口用途 接口地址 核心参数
当前天气 https://api.openweathermap.org/data/2.5/weather q(城市名)、appid(API Key)、units(单位)、lang(语言)
5天预报 https://api.openweathermap.org/data/2.5/forecast 同上
空气质量 https://api.openweathermap.org/data/2.5/air_pollution lat(纬度)、lon(经度)、appid

接口请求示例(当前天气)

https://api.openweathermap.org/data/2.5/weather?q=beijing,cn&appid=你的API_KEY&units=metric&lang=zh_cn
  • units=metric:返回摄氏度(imperial为华氏度)
  • lang=zh_cn:返回中文天气描述
  • q=beijing,cn:指定查询城市(格式:城市名,国家代码)

3. 接口返回格式解析

API返回JSON格式数据,核心字段说明:

{
  "coord": {"lon": 116.4, "lat": 39.9}, // 经纬度
  "weather": [{"id": 800, "main": "Clear", "description": "晴"}], // 天气核心信息
  "main": {
    "temp": 25,        // 当前温度
    "feels_like": 26,  // 体感温度
    "temp_min": 18,    // 最低温
    "temp_max": 28,    // 最高温
    "humidity": 65     // 湿度
  },
  "wind": {"speed": 3.5}, // 风速
  "sys": {
    "sunrise": 1759123800, // 日出时间戳
    "sunset": 1759170600   // 日落时间戳
  },
  "name": "北京" // 城市名
}

三、代码实现:从API调用到页面渲染

1. 核心封装:API请求函数

首先封装通用的API请求函数,处理异步请求和错误捕获:

// 替换为你的API Key
const API_KEY = "YOUR_OPENWEATHERMAP_API_KEY";
const BASE_URL = "https://api.openweathermap.org/data/2.5/";

// 获取当前天气数据
async function getCurrentWeather(city) {
  try {
    const response = await fetch(`${BASE_URL}weather?q=${city}&appid=${API_KEY}&units=metric&lang=zh_cn`);
    if (!response.ok) throw new Error(`城市 ${city} 未找到`);
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("获取天气失败:", error);
    alert(error.message);
    return null;
  }
}

// 获取5天预报数据(整理为按天分组)
async function getForecast(city) {
  try {
    const response = await fetch(`${BASE_URL}forecast?q=${city}&appid=${API_KEY}&units=metric&lang=zh_cn`);
    if (!response.ok) throw new Error("获取预报数据失败");
    const data = await response.json();
    
    // 按日期分组,提取每天核心数据
    const dailyForecast = {};
    data.list.forEach(item => {
      const date = new Date(item.dt * 1000).toLocaleDateString();
      if (!dailyForecast[date]) dailyForecast[date] = [];
      dailyForecast[date].push(item);
    });

    // 提取每天最高/最低温、天气描述
    return Object.keys(dailyForecast).map(date => {
      const dayData = dailyForecast[date];
      const temps = dayData.map(item => item.main.temp);
      return {
        date,
        maxTemp: Math.max(...temps),
        minTemp: Math.min(...temps),
        weather: dayData[0].weather[0],
        temp: dayData[Math.floor(dayData.length/2)].main.temp
      };
    }).slice(0, 6); // 取今天+未来5天
  } catch (error) {
    console.error("获取预报失败:", error);
    alert(error.message);
    return null;
  }
}

2. 数据渲染:将API数据更新到页面

(1)渲染当前天气

function renderCurrentWeather(data) {
  if (!data) return;

  // 显示加载状态
  document.getElementById('current-loading').style.display = 'block';
  document.getElementById('current-weather-content').style.display = 'none';

  // 解析数据(时间戳转本地时间)
  const sunrise = new Date(data.sys.sunrise * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
  const sunset = new Date(data.sys.sunset * 1000).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
  
  // 更新DOM元素
  document.getElementById('city-name').textContent = `${data.name}, ${data.sys.country}`;
  document.getElementById('current-temp').textContent = `${Math.round(data.main.temp)}°C`;
  document.getElementById('current-description').textContent = data.weather[0].description;
  document.getElementById('feels-like').textContent = `${Math.round(data.main.feels_like)}°C`;
  document.getElementById('humidity').textContent = `${data.main.humidity}%`;
  document.getElementById('wind').textContent = `${data.wind.speed} m/s`;
  document.getElementById('sunrise').textContent = sunrise;
  document.getElementById('sunset').textContent = sunset;

  // 更新背景和图标(根据天气类型)
  updateWeatherBackground(data.weather[0].main.toLowerCase());
  updateWeatherIcon(data.weather[0].main.toLowerCase());

  // 隐藏加载,显示内容
  document.getElementById('current-loading').style.display = 'none';
  document.getElementById('current-weather-content').style.display = 'block';
}

// 根据天气类型更新页面背景
function updateWeatherBackground(weatherType) {
  const body = document.body;
  body.classList.remove('clear-day', 'cloudy', 'rainy', 'snowy', 'foggy');
  switch(weatherType) {
    case 'clear': body.classList.add('clear-day'); break;
    case 'clouds': body.classList.add('cloudy'); break;
    case 'rain': body.classList.add('rainy'); break;
    case 'snow': body.classList.add('snowy'); break;
    case 'fog': body.classList.add('foggy'); break;
    default: body.classList.add('clear-day');
  }
}

(2)渲染温度趋势图表(Chart.js)

function renderTempChart(forecastData) {
  if (!forecastData) return;
  
  // 销毁原有图表,避免重复渲染
  if (window.tempChart) window.tempChart.destroy();

  // 处理图表标签和数据
  const labels = forecastData.map((item, idx) => {
    return idx === 0 ? '今天' : new Date(item.date).toLocaleDateString('zh-CN', {weekday: 'short'});
  });
  const maxTemps = forecastData.map(item => item.maxTemp);
  const minTemps = forecastData.map(item => item.minTemp);

  // 创建图表
  const ctx = document.getElementById('temperature-chart').getContext('2d');
  window.tempChart = new Chart(ctx, {
    type: 'line',
    data: {
      labels: labels,
      datasets: [
        {
          label: '最高温度 (°C)',
          data: maxTemps,
          borderColor: '#FF6B6B',
          backgroundColor: 'rgba(255, 107, 107, 0.1)',
          tension: 0.3,
          fill: true
        },
        {
          label: '最低温度 (°C)',
          data: minTemps,
          borderColor: '#4ECDC4',
          backgroundColor: 'rgba(78, 205, 196, 0.1)',
          tension: 0.3,
          fill: true
        }
      ]
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      scales: {
        y: { grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#fff' } },
        x: { grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: '#fff' } }
      },
      plugins: {
        legend: { labels: { color: '#fff' } },
        tooltip: { backgroundColor: 'rgba(0,0,0,0.7)', titleColor: '#fff' }
      }
    }
  });
}

3. 交互功能实现

(1)搜索与热门城市绑定

// 搜索按钮事件
document.getElementById('search-btn').addEventListener('click', async () => {
  const city = document.getElementById('city-input').value.trim();
  if (!city) { alert('请输入城市名称'); return; }
  
  // 获取并渲染数据
  const currentData = await getCurrentWeather(city);
  if (currentData) {
    renderCurrentWeather(currentData);
    const forecastData = await getForecast(city);
    if (forecastData) {
      renderForecast(forecastData); // 渲染未来预报(需自定义实现)
      renderTempChart(forecastData);
    }
  }
});

// 热门城市快速查询
document.querySelectorAll('.popular-city').forEach(btn => {
  btn.addEventListener('click', async () => {
    const city = btn.dataset.city;
    document.getElementById('city-input').value = city;
    const currentData = await getCurrentWeather(city);
    if (currentData) {
      renderCurrentWeather(currentData);
      const forecastData = await getForecast(city);
      if (forecastData) renderTempChart(forecastData);
    }
  });
});

(2)温度单位切换(摄氏度/华氏度)

// 单位转换核心逻辑
function convertTempUnit(isCelsius) {
  document.querySelectorAll('.temp, .forecast-temp, .feels-like').forEach(el => {
    if (el.textContent.includes('°')) {
      const num = parseFloat(el.textContent);
      let newTemp;
      if (isCelsius) {
        newTemp = Math.round((num - 32) * 5/9); // 华氏转摄氏
        el.textContent = `${newTemp}°C`;
      } else {
        newTemp = Math.round(num * 9/5 + 32); // 摄氏转华氏
        el.textContent = `${newTemp}°F`;
      }
    }
  });
}

// 绑定切换事件
document.getElementById('celsius-btn').addEventListener('click', () => {
  celsiusBtn.classList.add('active');
  fahrenheitBtn.classList.remove('active');
  convertTempUnit(true);
});

document.getElementById('fahrenheit-btn').addEventListener('click', () => {
  fahrenheitBtn.classList.add('active');
  celsiusBtn.classList.remove('active');
  convertTempUnit(false);
});

4. 视觉优化:响应式设计与动画

(1)响应式适配

通过CSS Media Query适配移动端和桌面端:

/* 平板适配 */
@media (max-width: 768px) {
  h1 { font-size: 2.2rem; }
  .current-weather { padding: 20px; }
  .forecast { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
}

/* 手机适配 */
@media (max-width: 480px) {
  h1 { font-size: 1.8rem; }
  .search-box { flex-direction: column; }
  #city-input, #search-btn { width: 100%; }
  .forecast { grid-template-columns: 1fr; }
}

(2)天气动画效果

为不同天气类型添加动态视觉效果:

/* 云朵移动动画 */
.cloud {
  position: absolute;
  width: 120px;
  height: 40px;
  background: rgba(255,255,255,0.8);
  border-radius: 50px;
  animation: cloud-move 20s linear infinite;
}

@keyframes cloud-move {
  0% { transform: translateX(-150px); }
  100% { transform: translateX(calc(100vw + 150px)); }
}

/* 下雨动画 */
.rain {
  width: 2px;
  height: 15px;
  background: rgba(255,255,255,0.7);
  animation: rain-fall 1s linear infinite;
}

@keyframes rain-fall {
  0% { transform: translateY(-50px); }
  100% { transform: translateY(100px); }
}

四、踩坑记录与解决方案

问题 原因 解决方法
API Key无效 新生成的Key未生效 等待10-20分钟,或重新生成Key
跨域报错 浏览器同源策略限制 使用Live Server插件,或配置CORS代理
时间显示错误 API返回UTC时间戳 new Date(timestamp * 1000)转换为本地时间
城市查询失败 城市名格式错误 统一使用「城市名,国家代码」格式(如shanghai,cn)

五、完整代码

以下是整合所有功能的完整HTML代码(需替换API Key):

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>城市天气预报查询</title>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        /* 此处省略完整CSS样式,可使用原文中的样式代码 */
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        }
        
        body {
            background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
            background-size: 400% 400%;
            color: #fff;
            min-height: 100vh;
            padding: 20px;
            transition: background 1.5s ease;
        }
        
        /* 其余样式代码请参考原文 */
    </style>
</head>
<body class="clear-day">
    <div class="container">
        <!-- HTML结构部分请参考原文 -->
    </div>

    <script>
        // 替换为你的API Key
        const API_KEY = "YOUR_API_KEY";
        const BASE_URL = "https://api.openweathermap.org/data/2.5/";

        // 此处粘贴上述所有JavaScript函数
        // 包括getCurrentWeather、getForecast、renderCurrentWeather等
    </script>
</body>
</html>

总结

核心知识点回顾

  1. API配置核心:获取有效API Key是基础,理解接口参数(单位、语言)和返回格式是关键
  2. 数据处理重点:需将API返回的原始数据(如时间戳、数组)转换为页面展示所需格式
  3. 交互体验优化:响应式设计、天气动画、单位切换等细节能显著提升用户体验
  4. 错误处理不可少:捕获API调用失败、城市不存在等异常,增强程序鲁棒性

扩展方向

这个基础版本还有很多可优化的地方:

  • 增加地理位置自动定位(Geolocation API)
  • 完善空气质量显示(调用air_pollution接口)
  • 加入本地存储(localStorage)保存历史查询记录
  • 增加天气预警、生活建议等个性化功能
posted @ 2026-01-15 00:48  Moonbeamsc  阅读(3)  评论(0)    收藏  举报
返回顶端