Day 30: 项目6-天气查询应用
🎯 项目概述
我们将综合运用前29天学到的所有知识,构建一个功能完整、界面精美的天气查询应用。这是编程入门教程的终极项目,也是你JavaScript开发能力的综合检验。
项目特点
- ✅ 真实API集成:使用OpenWeatherMap API获取实时天气
- ✅ 完整CRUD操作:城市增删改查
- ✅ 本地存储:保存偏好设置和历史记录
- ✅ 异步编程:fetch + async/await
- ✅ 响应式设计:适配桌面和移动设备
- ✅ 动画效果:流畅的UI交互
- ✅ 错误处理:友好的错误提示
- ✅ 代码规范:模块化、可维护
🎨 项目预览
功能清单
-
实时天气查询
- 支持城市名称搜索
- 显示当前温度、天气状况
- 湿度、风速、气压等详细信息
- 天气图标动态显示
-
多城市管理
- 添加/删除常用城市
- 城市列表快速切换
- 搜索历史记录
-
数据持久化
- LocalStorage保存设置
- 自动恢复上次状态
-
用户体验
- 加载动画
- 错误提示
- 回车快捷搜索
- 防抖优化
📁 项目结构
weather-app/
├── index.html # 主页面
├── css/
│ ├── styles.css # 主样式
│ └── animations.css # 动画效果
├── js/
│ ├── api.js # API请求封装
│ ├── storage.js # 本地存储管理
│ ├── ui.js # UI交互逻辑
│ └── app.js # 主应用逻辑
└── assets/
└── icons/ # 天气图标
💻 完整代码实现
1. HTML结构 (index.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>
<link rel="stylesheet" href="css/styles.css">
<link rel="stylesheet" href="css/animations.css">
</head>
<body>
<div class="container">
<!-- 头部 -->
<header class="header">
<h1 class="title">🌤️ 天气查询</h1>
<p class="subtitle">实时天气信息查询</p>
</header>
<!-- 搜索区域 -->
<section class="search-section">
<div class="search-box">
<input
type="text"
id="cityInput"
class="search-input"
placeholder="输入城市名称..."
autocomplete="off"
>
<button id="searchBtn" class="search-btn">
<span class="btn-text">查询</span>
<span class="btn-icon">🔍</span>
</button>
</div>
<div class="search-tips">
💡 提示:支持中英文城市名,如"北京"、"Tokyo"
</div>
</section>
<!-- 加载动画 -->
<div id="loading" class="loading hidden">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<!-- 错误提示 -->
<div id="error" class="error-message hidden"></div>
<!-- 天气信息卡片 -->
<section id="weatherCard" class="weather-card hidden">
<!-- 城市和天气图标 -->
<div class="weather-header">
<div>
<h2 class="city-name" id="cityName">--</h2>
<p class="weather-date" id="weatherDate">--</p>
</div>
<div class="weather-icon" id="weatherIcon">🌡️</div>
</div>
<!-- 温度 -->
<div class="temperature-section">
<span class="temperature" id="temperature">--°</span>
<span class="weather-description" id="description">--</span>
</div>
<!-- 详细信息网格 -->
<div class="weather-details">
<div class="detail-item">
<div class="detail-icon">💧</div>
<div class="detail-info">
<div class="detail-label">湿度</div>
<div class="detail-value" id="humidity">--%</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">💨</div>
<div class="detail-info">
<div class="detail-label">风速</div>
<div class="detail-value" id="windSpeed">-- m/s</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">🌡️</div>
<div class="detail-info">
<div class="detail-label">体感温度</div>
<div class="detail-value" id="feelsLike">--°</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">🔵</div>
<div class="detail-info">
<div class="detail-label">气压</div>
<div class="detail-value" id="pressure">-- hPa</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">👁️</div>
<div class="detail-info">
<div class="detail-label">能见度</div>
<div class="detail-value" id="visibility">-- km</div>
</div>
</div>
<div class="detail-item">
<div class="detail-icon">☁️</div>
<div class="detail-info">
<div class="detail-label">云量</div>
<div class="detail-value" id="clouds">--%</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="weather-actions">
<button id="saveCityBtn" class="action-btn">
⭐ 保存城市
</button>
<button id="refreshBtn" class="action-btn">
🔄 刷新
</button>
</div>
</section>
<!-- 已保存的城市 -->
<section class="saved-cities-section">
<h3 class="section-title">📍 已保存的城市</h3>
<div id="savedCities" class="saved-cities-list">
<p class="empty-message">暂无保存的城市</p>
</div>
</section>
<!-- 搜索历史 -->
<section class="history-section">
<h3 class="section-title">📜 搜索历史</h3>
<div id="searchHistory" class="history-list">
<p class="empty-message">暂无搜索历史</p>
</div>
<button id="clearHistoryBtn" class="clear-btn">
🗑️ 清空历史
</button>
</section>
<!-- 页脚 -->
<footer class="footer">
<p>数据来源:<a href="https://openweathermap.org/" target="_blank">OpenWeatherMap</a></p>
<p>© 2026 天气查询应用 | 编程入门教程 Day 30</p>
</footer>
</div>
<!-- JavaScript模块 -->
<script type="module" src="js/app.js"></script>
</body>
</html>
2. CSS样式 (css/styles.css)
/* ==================== 全局样式 ==================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--accent-color: #f093fb;
--text-dark: #2d3748;
--text-light: #718096;
--bg-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--card-bg: rgba(255, 255, 255, 0.95);
--shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
--radius: 16px;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-gradient);
min-height: 100vh;
padding: 20px;
color: var(--text-dark);
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
}
/* ==================== 头部样式 ==================== */
.header {
text-align: center;
margin-bottom: 40px;
color: white;
}
.title {
font-size: 2.5em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
}
/* ==================== 搜索区域 ==================== */
.search-section {
background: var(--card-bg);
padding: 30px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 30px;
}
.search-box {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.search-input {
flex: 1;
padding: 15px 20px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s;
}
.search-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-btn {
padding: 15px 30px;
background: var(--bg-gradient);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.search-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.search-btn:active {
transform: translateY(0);
}
.search-tips {
font-size: 14px;
color: var(--text-light);
text-align: center;
}
/* ==================== 加载和错误 ==================== */
.loading {
text-align: center;
padding: 40px;
color: white;
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.3);
border-top: 4px solid white;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-message {
background: #fed7d7;
color: #c53030;
padding: 15px 20px;
border-radius: 10px;
margin-bottom: 20px;
border-left: 4px solid #c53030;
}
/* ==================== 天气卡片 ==================== */
.weather-card {
background: var(--card-bg);
padding: 40px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 30px;
}
.weather-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
}
.city-name {
font-size: 2em;
color: var(--text-dark);
margin-bottom: 5px;
}
.weather-date {
color: var(--text-light);
font-size: 0.9em;
}
.weather-icon {
font-size: 4em;
}
.temperature-section {
text-align: center;
margin-bottom: 40px;
}
.temperature {
font-size: 4em;
font-weight: bold;
background: var(--bg-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.weather-description {
font-size: 1.5em;
color: var(--text-light);
text-transform: capitalize;
margin-top: 10px;
}
.weather-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.detail-item {
background: #f7fafc;
padding: 20px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 15px;
transition: transform 0.2s;
}
.detail-item:hover {
transform: translateY(-3px);
background: #edf2f7;
}
.detail-icon {
font-size: 2em;
}
.detail-label {
font-size: 0.85em;
color: var(--text-light);
margin-bottom: 5px;
}
.detail-value {
font-size: 1.2em;
font-weight: bold;
color: var(--text-dark);
}
.weather-actions {
display: flex;
gap: 15px;
}
.action-btn {
flex: 1;
padding: 15px;
background: var(--bg-gradient);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
/* ==================== 已保存城市 ==================== */
.saved-cities-section,
.history-section {
background: var(--card-bg);
padding: 30px;
border-radius: var(--radius);
box-shadow: var(--shadow);
margin-bottom: 20px;
}
.section-title {
font-size: 1.3em;
margin-bottom: 20px;
color: var(--text-dark);
}
.saved-cities-list,
.history-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.city-tag,
.history-tag {
background: #edf2f7;
padding: 10px 20px;
border-radius: 25px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
}
.city-tag:hover,
.history-tag:hover {
background: #e2e8f0;
transform: translateY(-2px);
}
.city-tag .delete-btn {
background: none;
border: none;
color: #e53e3e;
cursor: pointer;
font-size: 16px;
padding: 0;
margin-left: 5px;
}
.city-tag .delete-btn:hover {
color: #c53030;
}
.empty-message {
color: var(--text-light);
font-style: italic;
}
.clear-btn {
margin-top: 15px;
padding: 10px 20px;
background: #fed7d7;
color: #c53030;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.clear-btn:hover {
background: #fc8181;
color: white;
}
/* ==================== 页脚 ==================== */
.footer {
text-align: center;
color: white;
padding: 20px;
opacity: 0.9;
}
.footer a {
color: white;
text-decoration: underline;
}
/* ==================== 工具类 ==================== */
.hidden {
display: none !important;
}
/* ==================== 响应式设计 ==================== */
@media (max-width: 600px) {
.title {
font-size: 2em;
}
.search-box {
flex-direction: column;
}
.temperature {
font-size: 3em;
}
.weather-details {
grid-template-columns: 1fr;
}
.weather-actions {
flex-direction: column;
}
}
3. JavaScript模块 (js/api.js)
// ==================== API配置 ====================
const API_KEY = 'YOUR_OPENWEATHERMAP_API_KEY'; // 替换为你的API密钥
const API_BASE = 'https://api.openweathermap.org/data/2.5/weather';
// ==================== API请求封装 ====================
/**
* 获取天气数据
* @param {string} city - 城市名称
* @returns {Promise<Object>} 天气数据
*/
export async function getWeather(city) {
const url = `${API_BASE}?q=${encodeURIComponent(city)}&units=metric&lang=zh_cn&appid=${API_KEY}`;
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error('未找到该城市,请检查城市名称');
} else if (response.status === 401) {
throw new Error('API密钥无效');
} else {
throw new Error(`请求失败: ${response.status}`);
}
}
return await response.json();
}
/**
* 获取多个城市的天气
* @param {string[]} cities - 城市列表
* @returns {Promise<Object[]>} 天气数据数组
*/
export async function getWeatherForCities(cities) {
const promises = cities.map(city => getWeather(city));
return Promise.allSettled(promises);
}
4. JavaScript模块 (js/storage.js)
// ==================== 本地存储管理 ====================
const STORAGE_KEYS = {
SAVED_CITIES: 'weatherapp_saved_cities',
SEARCH_HISTORY: 'weatherapp_search_history',
LAST_CITY: 'weatherapp_last_city'
};
/**
* 保存城市列表
* @param {string[]} cities - 城市列表
*/
export function saveCities(cities) {
localStorage.setItem(STORAGE_KEYS.SAVED_CITIES, JSON.stringify(cities));
}
/**
* 获取保存的城市列表
* @returns {string[]} 城市列表
*/
export function getSavedCities() {
const data = localStorage.getItem(STORAGE_KEYS.SAVED_CITIES);
return data ? JSON.parse(data) : [];
}
/**
* 添加城市到保存列表
* @param {string} city - 城市名称
*/
export function addCity(city) {
const cities = getSavedCities();
if (!cities.includes(city)) {
cities.push(city);
saveCities(cities);
}
}
/**
* 从保存列表中删除城市
* @param {string} city - 城市名称
*/
export function removeCity(city) {
const cities = getSavedCities().filter(c => c !== city);
saveCities(cities);
}
/**
* 保存搜索历史
* @param {string} city - 城市名称
*/
export function addToHistory(city) {
const history = getSearchHistory();
// 移除重复项
const filtered = history.filter(c => c !== city);
// 添加到开头
filtered.unshift(city);
// 限制数量
const limited = filtered.slice(0, 10);
localStorage.setItem(STORAGE_KEYS.SEARCH_HISTORY, JSON.stringify(limited));
}
/**
* 获取搜索历史
* @returns {string[]} 搜索历史
*/
export function getSearchHistory() {
const data = localStorage.getItem(STORAGE_KEYS.SEARCH_HISTORY);
return data ? JSON.parse(data) : [];
}
/**
* 清空搜索历史
*/
export function clearHistory() {
localStorage.removeItem(STORAGE_KEYS.SEARCH_HISTORY);
}
/**
* 保存最后查询的城市
* @param {string} city - 城市名称
*/
export function saveLastCity(city) {
localStorage.setItem(STORAGE_KEYS.LAST_CITY, city);
}
/**
* 获取最后查询的城市
* @returns {string|null} 城市名称
*/
export function getLastCity() {
return localStorage.getItem(STORAGE_KEYS.LAST_CITY);
}
5. JavaScript模块 (js/ui.js)
// ==================== DOM元素 ====================
const elements = {
cityInput: document.getElementById('cityInput'),
searchBtn: document.getElementById('searchBtn'),
loading: document.getElementById('loading'),
error: document.getElementById('error'),
weatherCard: document.getElementById('weatherCard'),
cityName: document.getElementById('cityName'),
weatherDate: document.getElementById('weatherDate'),
weatherIcon: document.getElementById('weatherIcon'),
temperature: document.getElementById('temperature'),
description: document.getElementById('description'),
humidity: document.getElementById('humidity'),
windSpeed: document.getElementById('windSpeed'),
feelsLike: document.getElementById('feelsLike'),
pressure: document.getElementById('pressure'),
visibility: document.getElementById('visibility'),
clouds: document.getElementById('clouds'),
saveCityBtn: document.getElementById('saveCityBtn'),
refreshBtn: document.getElementById('refreshBtn'),
savedCities: document.getElementById('savedCities'),
searchHistory: document.getElementById('searchHistory'),
clearHistoryBtn: document.getElementById('clearHistoryBtn')
};
// ==================== 天气图标映射 ====================
const weatherIcons = {
'01d': '☀️', '01n': '🌙',
'02d': '⛅', '02n': '☁️',
'03d': '☁️', '03n': '☁️',
'04d': '☁️', '04n': '☁️',
'09d': '🌧️', '09n': '🌧️',
'10d': '🌦️', '10n': '🌧️',
'11d': '⛈️', '11n': '⛈️',
'13d': '❄️', '13n': '❄️',
'50d': '🌫️', '50n': '🌫️'
};
// ==================== UI操作函数 ====================
/**
* 显示加载状态
*/
export function showLoading() {
elements.loading.classList.remove('hidden');
elements.error.classList.add('hidden');
elements.weatherCard.classList.add('hidden');
}
/**
* 隐藏加载状态
*/
export function hideLoading() {
elements.loading.classList.add('hidden');
}
/**
* 显示错误信息
* @param {string} message - 错误信息
*/
export function showError(message) {
elements.error.textContent = message;
elements.error.classList.remove('hidden');
elements.weatherCard.classList.add('hidden');
}
/**
* 显示天气信息
* @param {Object} data - 天气数据
*/
export function showWeather(data) {
const { main, weather, wind, visibility, clouds, name } = data;
// 更新基本信息
elements.cityName.textContent = name;
elements.weatherDate.textContent = formatDate();
// 更新天气图标
const iconCode = weather[0].icon;
elements.weatherIcon.textContent = weatherIcons[iconCode] || '🌡️';
// 更新温度
elements.temperature.textContent = `${Math.round(main.temp)}°C`;
elements.description.textContent = weather[0].description;
// 更新详细信息
elements.humidity.textContent = `${main.humidity}%`;
elements.windSpeed.textContent = `${wind.speed} m/s`;
elements.feelsLike.textContent = `${Math.round(main.feels_like)}°C`;
elements.pressure.textContent = `${main.pressure} hPa`;
elements.visibility.textContent = `${(visibility / 1000).toFixed(1)} km`;
elements.clouds.textContent = `${clouds.all}%`;
// 显示天气卡片
elements.error.classList.add('hidden');
elements.weatherCard.classList.remove('hidden');
}
/**
* 格式化日期
* @returns {string} 格式化的日期字符串
*/
function formatDate() {
const now = new Date();
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: '2-digit',
minute: '2-digit'
};
return now.toLocaleDateString('zh-CN', options);
}
/**
* 更新已保存城市列表
* @param {string[]} cities - 城市列表
* @param {Function} onCityClick - 城市点击回调
* @param {Function} onDeleteClick - 删除按钮点击回调
*/
export function updateSavedCities(cities, onCityClick, onDeleteClick) {
if (cities.length === 0) {
elements.savedCities.innerHTML = '<p class="empty-message">暂无保存的城市</p>';
return;
}
elements.savedCities.innerHTML = cities.map(city => `
<div class="city-tag" data-city="${city}">
<span>${city}</span>
<button class="delete-btn" data-city="${city}">×</button>
</div>
`).join('');
// 添加事件监听
elements.savedCities.querySelectorAll('.city-tag').forEach(tag => {
tag.addEventListener('click', (e) => {
if (!e.target.classList.contains('delete-btn')) {
onCityClick(tag.dataset.city);
}
});
});
elements.savedCities.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
onDeleteClick(btn.dataset.city);
});
});
}
/**
* 更新搜索历史
* @param {string[]} history - 搜索历史
* @param {Function} onHistoryClick - 历史项点击回调
*/
export function updateSearchHistory(history, onHistoryClick) {
if (history.length === 0) {
elements.searchHistory.innerHTML = '<p class="empty-message">暂无搜索历史</p>';
return;
}
elements.searchHistory.innerHTML = history.map(city => `
<div class="history-tag" data-city="${city}">
${city}
</div>
`).join('');
// 添加事件监听
elements.searchHistory.querySelectorAll('.history-tag').forEach(tag => {
tag.addEventListener('click', () => {
onHistoryClick(tag.dataset.city);
});
});
}
/**
* 导出DOM元素
*/
export { elements };
6. JavaScript主应用 (js/app.js)
import { getWeather } from './api.js';
import * as storage from './storage.js';
import * as ui from './ui.js';
import { elements } from './ui.js';
// ==================== 防抖函数 ====================
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// ==================== 查询天气 ====================
async function searchWeather(city) {
if (!city || city.trim() === '') {
ui.showError('请输入城市名称');
return;
}
ui.showLoading();
try {
const data = await getWeather(city);
ui.showWeather(data);
storage.addToHistory(city);
storage.saveLastCity(city);
updateUI();
} catch (error) {
ui.showError(error.message);
} finally {
ui.hideLoading();
}
}
// ==================== 保存城市 ====================
function saveCurrentCity() {
const city = elements.cityName.textContent;
if (city && city !== '--') {
storage.addCity(city);
updateUI();
}
}
// ==================== 删除城市 ====================
function deleteCity(city) {
storage.removeCity(city);
updateUI();
}
// ==================== 清空历史 ====================
function clearHistory() {
if (confirm('确定要清空搜索历史吗?')) {
storage.clearHistory();
updateUI();
}
}
// ==================== 更新UI ====================
function updateUI() {
// 更新已保存城市
const savedCities = storage.getSavedCities();
ui.updateSavedCities(savedCities, searchWeather, deleteCity);
// 更新搜索历史
const history = storage.getSearchHistory();
ui.updateSearchHistory(history, searchWeather);
}
// ==================== 事件监听 ====================
elements.searchBtn.addEventListener('click', () => {
const city = elements.cityInput.value.trim();
searchWeather(city);
});
// 回车键搜索
elements.cityInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchWeather(elements.cityInput.value.trim());
}
});
// 保存城市
elements.saveCityBtn.addEventListener('click', saveCurrentCity);
// 刷新天气
elements.refreshBtn.addEventListener('click', () => {
const city = elements.cityName.textContent;
if (city && city !== '--') {
searchWeather(city);
}
});
// 清空历史
elements.clearHistoryBtn.addEventListener('click', clearHistory);
// ==================== 初始化 ====================
function init() {
updateUI();
// 恢复上次查询的城市
const lastCity = storage.getLastCity();
if (lastCity) {
elements.cityInput.value = lastCity;
searchWeather(lastCity);
}
// 聚焦输入框
elements.cityInput.focus();
}
// 启动应用
init();
⚠️ 重要注意事项
1. API密钥安全
⚠️ 不要在前端代码中硬编码API密钥!
这个项目为了演示方便,直接在代码中使用了API密钥。但在生产环境中,你应该:
- 使用代理服务器
// 前端
const response = await fetch('/api/weather?city=Beijing');
// 后端(Node.js)
app.get('/api/weather', async (req, res) => {
const city = req.query.city;
const apiKey = process.env.OPENWEATHERMAP_API_KEY; // 从环境变量读取
const url = `${API_BASE}?q=${city}&appid=${apiKey}`;
const response = await fetch(url);
const data = await response.json();
res.json(data);
});
2. 错误处理
关键错误场景:
- 网络请求失败
- API密钥无效
- 城市名称错误
- 服务器超时
处理策略:
try {
const data = await getWeather(city);
ui.showWeather(data);
} catch (error) {
if (error.message.includes('Failed to fetch')) {
ui.showError('网络连接失败,请检查网络');
} else if (error.message.includes('404')) {
ui.showError('未找到该城市');
} else {
ui.showError('查询失败,请稍后重试');
}
}
3. 性能优化
防抖:避免频繁的API请求
const debouncedSearch = debounce(searchWeather, 500);
elements.cityInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
缓存:减少重复请求
const cache = new Map();
async function getCachedWeather(city) {
if (cache.has(city)) {
const cached = cache.get(city);
if (Date.now() - cached.timestamp < 600000) { // 10分钟缓存
return cached.data;
}
}
const data = await getWeather(city);
cache.set(city, { data, timestamp: Date.now() });
return data;
}
✍️ 练习任务
基础练习
-
运行项目
- 替换API密钥
- 在本地服务器运行项目
- 测试所有功能
-
自定义样式
- 修改颜色主题
- 调整布局和间距
进阶练习
-
添加新功能
- 5天天气预报
- 城市地图定位
- 天气图表
-
性能优化
- 实现请求缓存
- 添加加载动画优化
实战练习
- 部署到生产
- 使用Netlify/Vercel部署
- 配置环境变量
- 设置CDN
🎓 项目总结
恭喜你完成了编程入门教程的终极项目!
你已经掌握
基础知识:
- ✅ JavaScript核心语法
- ✅ DOM操作和事件处理
- ✅ CSS布局和动画
- ✅ 异步编程
实战技能:
- ✅ API集成
- ✅ 本地存储
- ✅ 错误处理
- ✅ 响应式设计
- ✅ 模块化开发
下一步建议
-
深入学习
- TypeScript(类型系统)
- React/Vue(前端框架)
- Node.js(后端开发)
-
项目实战
- 构建自己的作品集
- 参与开源项目
- 开发实用工具
-
持续学习
- 关注技术趋势
- 阅读优秀代码
- 加入开发者社区
🎉 恭喜完成30天编程入门之旅!
继续编程,创造精彩! 🚀