Day 29: API请求fetch
🎯 学习目标
- 理解什么是API以及HTTP协议基础
- 掌握fetch API的基本用法
- 学会处理GET、POST等常见请求
- 了解异步数据获取和错误处理
- 实现真实的天气查询应用
💡 核心概念
什么是API?
API(Application Programming Interface,应用程序编程接口)是不同软件系统之间通信的桥梁。在Web开发中,我们最常用的是Web API,它通过HTTP协议传输数据。
现实类比:
- API就像餐厅的服务员
- 你(客户端)告诉服务员想要什么菜(请求)
- 服务员去厨房(服务器)取菜
- 最后把菜端给你(响应)
HTTP协议基础
HTTP请求方法:
GET - 获取数据(安全、幂等)
POST - 创建新资源
PUT - 更新整个资源
PATCH - 部分更新资源
DELETE - 删除资源
HTTP状态码:
200 - 成功 ✅
201 - 已创建 ✅
400 - 错误请求 ❌
401 - 未授权 ❌
403 - 禁止访问 ❌
404 - 未找到 ❌
500 - 服务器错误 ❌
Fetch API是什么?
Fetch API是现代浏览器提供的网络请求接口,它基于Promise设计,比传统的XMLHttpRequest更简洁、更强大。
基本语法:
fetch(url, options)
.then(response => response.json()) // 解析JSON
.then(data => console.log(data)) // 处理数据
.catch(error => console.error(error)); // 处理错误
📝 Fetch API详解
1. GET请求 – 获取数据
基本示例:
// 获取用户信息
fetch('https://jsonplaceholder.typicode.com/users/1')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(user => {
console.log('用户数据:', user);
displayUserInfo(user);
})
.catch(error => {
console.error('请求失败:', error);
});
带参数的GET请求:
// 使用URLSearchParams构建查询字符串
const params = new URLSearchParams({
userId: 1,
status: 'active'
});
fetch(`https://api.example.com/users?${params}`)
.then(response => response.json())
.then(data => console.log(data));
// 或者手动拼接
fetch('https://api.example.com/users?userId=1&status=active')
.then(response => response.json())
.then(data => console.log(data));
2. POST请求 – 创建数据
发送JSON数据:
fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'foo',
body: 'bar',
userId: 1,
}),
})
.then(response => response.json())
.then(data => {
console.log('成功创建:', data);
// 服务器返回的新创建的资源
// { id: 101, title: 'foo', body: 'bar', userId: 1 }
});
发送表单数据:
const formData = new FormData();
formData.append('username', 'john');
formData.append('email', 'john@example.com');
fetch('https://api.example.com/users', {
method: 'POST',
body: formData, // 自动设置Content-Type为multipart/form-data
})
.then(response => response.json())
.then(data => console.log(data));
3. PUT请求 – 更新数据
fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: 1,
title: 'updated title',
body: 'updated body',
userId: 1,
}),
})
.then(response => response.json())
.then(data => console.log('更新成功:', data));
4. DELETE请求 – 删除数据
fetch('https://jsonplaceholder.typicode.com/posts/1', {
method: 'DELETE',
})
.then(response => {
if (response.ok) {
console.log('删除成功');
}
});
5. 常用Request选项
fetch('https://api.example.com/data', {
// HTTP方法
method: 'GET', // 或 'POST', 'PUT', 'DELETE' 等
// 请求头
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_TOKEN',
'Accept': 'application/json',
},
// 请求体
body: JSON.stringify({ key: 'value' }),
// 请求模式
mode: 'cors', // 'cors', 'no-cors', 'same-origin'
// 凭证模式
credentials: 'include', // 'include', 'same-origin', 'omit'
// 缓存模式
cache: 'default', // 'default', 'no-cache', 'reload', 'force-cache'
// 重定向模式
redirect: 'follow', // 'follow', 'error', 'manual'
// 引用者
referrer: 'no-referrer', // 'client', 'no-referrer'
// 优先级
priority: 'high', // 'high', 'low', 'auto'
});
🎮 实战示例:天气查询应用
完整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: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 40px;
max-width: 500px;
width: 100%;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 2em;
}
.search-box {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
#cityInput {
flex: 1;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
#cityInput:focus {
outline: none;
border-color: #667eea;
}
#searchBtn {
padding: 15px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
#searchBtn:hover {
transform: scale(1.05);
}
#searchBtn:active {
transform: scale(0.95);
}
.weather-info {
display: none;
text-align: center;
}
.weather-info.active {
display: block;
}
.weather-icon {
font-size: 80px;
margin-bottom: 20px;
}
.temperature {
font-size: 48px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
}
.description {
font-size: 20px;
color: #666;
margin-bottom: 20px;
text-transform: capitalize;
}
.details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
margin-top: 20px;
}
.detail-item {
background: #f5f5f5;
padding: 15px;
border-radius: 10px;
}
.detail-label {
font-size: 12px;
color: #999;
margin-bottom: 5px;
}
.detail-value {
font-size: 18px;
font-weight: bold;
color: #333;
}
.loading {
text-align: center;
padding: 20px;
font-size: 18px;
color: #666;
}
.error {
background: #ff4444;
color: white;
padding: 15px;
border-radius: 10px;
text-align: center;
margin-top: 20px;
}
.history {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
}
.history-title {
font-size: 16px;
font-weight: bold;
color: #333;
margin-bottom: 15px;
}
.history-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.history-item {
background: #f0f0f0;
padding: 8px 15px;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.history-item:hover {
background: #e0e0e0;
}
</style>
</head>
<body>
<div class="container">
<h1>🌤️ 天气查询</h1>
<div class="search-box">
<input
type="text"
id="cityInput"
placeholder="输入城市名称..."
autocomplete="off"
>
<button id="searchBtn">查询</button>
</div>
<div id="loading" class="loading" style="display: none;">
加载中...
</div>
<div id="error" class="error" style="display: none;"></div>
<div id="weatherInfo" class="weather-info">
<div class="weather-icon" id="weatherIcon">☀️</div>
<div class="temperature" id="temperature">--°C</div>
<div class="description" id="description">--</div>
<div class="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">-- m/s</div>
</div>
<div class="detail-item">
<div class="detail-label">体感温度</div>
<div class="detail-value" id="feelsLike">--°C</div>
</div>
<div class="detail-item">
<div class="detail-label">气压</div>
<div class="detail-value" id="pressure">-- hPa</div>
</div>
</div>
</div>
<div class="history">
<div class="history-title">📍 搜索历史</div>
<div class="history-list" id="historyList">
<span style="color: #999; font-size: 14px;">暂无历史记录</span>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
完整JavaScript逻辑
// ===== 应用状态 =====
let searchHistory = [];
// ===== DOM元素 =====
const cityInput = document.getElementById('cityInput');
const searchBtn = document.getElementById('searchBtn');
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const weatherInfo = document.getElementById('weatherInfo');
const historyList = document.getElementById('historyList');
// ===== 天气图标映射 =====
const weatherIcons = {
'01d': '☀️', // 晴天
'01n': '🌙',
'02d': '⛅', // 多云
'02n': '☁️',
'03d': '☁️', // 阴天
'03n': '☁️',
'04d': '☁️',
'04n': '☁️',
'09d': '🌧️', // 小雨
'09n': '🌧️',
'10d': '🌦️', // 雨天
'10n': '🌧️',
'11d': '⛈️', // 雷阵雨
'11n': '⛈️',
'13d': '❄️', // 雪
'13n': '❄️',
'50d': '🌫️', // 雾
'50n': '🌫️',
};
// ===== API配置 =====
const API_KEY = 'YOUR_OPENWEATHERMAP_API_KEY'; // 需要替换为真实API密钥
const API_BASE = 'https://api.openweathermap.org/data/2.5/weather';
// ===== 获取天气数据 =====
async function getWeather(city) {
// 显示加载状态
loading.style.display = 'block';
error.style.display = 'none';
weatherInfo.classList.remove('active');
try {
// 发送API请求
const response = await fetch(
`${API_BASE}?q=${encodeURIComponent(city)}&units=metric&lang=zh_cn&appid=${API_KEY}`
);
// 检查响应状态
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}`);
}
}
// 解析JSON数据
const data = await response.json();
// 显示天气信息
displayWeather(data);
// 添加到历史记录
addToHistory(city);
} catch (err) {
// 显示错误信息
showError(err.message);
} finally {
// 隐藏加载状态
loading.style.display = 'none';
}
}
// ===== 显示天气信息 =====
function displayWeather(data) {
const { main, weather, wind, name } = data;
// 更新天气图标
const iconCode = weather[0].icon;
document.getElementById('weatherIcon').textContent =
weatherIcons[iconCode] || '🌡️';
// 更新温度
document.getElementById('temperature').textContent =
`${Math.round(main.temp)}°C`;
// 更新天气描述
document.getElementById('description').textContent =
weather[0].description;
// 更新详细信息
document.getElementById('humidity').textContent = `${main.humidity}%`;
document.getElementById('windSpeed').textContent = `${wind.speed} m/s`;
document.getElementById('feelsLike').textContent =
`${Math.round(main.feels_like)}°C`;
document.getElementById('pressure').textContent = `${main.pressure} hPa`;
// 显示天气信息
weatherInfo.classList.add('active');
}
// ===== 显示错误信息 =====
function showError(message) {
error.textContent = message;
error.style.display = 'block';
}
// ===== 添加到历史记录 =====
function addToHistory(city) {
// 移除重复项
searchHistory = searchHistory.filter(item => item !== city);
// 添加到开头
searchHistory.unshift(city);
// 限制历史记录数量
if (searchHistory.length > 10) {
searchHistory = searchHistory.slice(0, 10);
}
// 更新显示
updateHistoryDisplay();
// 保存到本地存储
localStorage.setItem('weatherHistory', JSON.stringify(searchHistory));
}
// ===== 更新历史记录显示 =====
function updateHistoryDisplay() {
if (searchHistory.length === 0) {
historyList.innerHTML =
'<span style="color: #999; font-size: 14px;">暂无历史记录</span>';
return;
}
historyList.innerHTML = searchHistory
.map(city => `<div class="history-item" onclick="searchCity('${city}')">${city}</div>`)
.join('');
}
// ===== 从历史记录搜索 =====
function searchCity(city) {
cityInput.value = city;
getWeather(city);
}
// ===== 加载历史记录 =====
function loadHistory() {
const saved = localStorage.getItem('weatherHistory');
if (saved) {
searchHistory = JSON.parse(saved);
updateHistoryDisplay();
}
}
// ===== 事件监听 =====
searchBtn.addEventListener('click', () => {
const city = cityInput.value.trim();
if (city) {
getWeather(city);
} else {
showError('请输入城市名称');
}
});
// 回车键搜索
cityInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
searchBtn.click();
}
});
// 页面加载时恢复历史记录
loadHistory();
// 聚焦输入框
cityInput.focus();
如何获取API密钥
- 访问 https://openweathermap.org/api
- 注册账号(免费)
- 在API keys页面获取密钥
- 将代码中的
YOUR_OPENWEATHERMAP_API_KEY替换为你的密钥
⚠️ 重要注意事项
1. CORS(跨域资源共享)
什么是CORS?
浏览器的安全策略,限制网页向不同域的服务器发送请求。
常见CORS错误:
Access to fetch at 'https://api.example.com' from origin 'https://yoursite.com'
has been blocked by CORS policy
解决方案:
- 服务器端设置正确的CORS头
- 使用代理服务器
- 开发环境可以使用CORS插件(仅用于测试)
2. 错误处理最佳实践
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, options);
// 检查HTTP状态码
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(
errorData.message || `HTTP ${response.status}: ${response.statusText}`
);
}
// 检查Content-Type
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error('响应不是JSON格式');
}
return await response.json();
} catch (error) {
// 网络错误或JSON解析错误
if (error instanceof TypeError) {
throw new Error('网络连接失败');
}
throw error;
}
}
// 使用示例
fetchWithErrorHandling('https://api.example.com/data')
.then(data => console.log(data))
.catch(error => console.error('错误:', error.message));
3. 请求超时处理
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
),
]);
}
// 使用示例
fetchWithTimeout('https://api.example.com/data', {}, 3000)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('错误:', error.message));
4. 取消请求(AbortController)
const controller = new AbortController();
// 5秒后自动取消
setTimeout(() => controller.abort(), 5000);
fetch('https://api.example.com/data', {
signal: controller.signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('其他错误:', error);
}
});
// 手动取消
// controller.abort();
5. API密钥安全
⚠️ 永远不要在前端代码中硬编码敏感的API密钥!
为什么?
- 前端代码对所有人可见
- 任何人都可以查看源代码窃取密钥
正确做法:
- 使用代理服务器转发请求
- 服务器端保存密钥
- 前端只请求自己的服务器
示例:
// ❌ 错误做法
const API_KEY = 'sk-1234567890abcdef';
fetch('https://api.example.com/data?key=' + API_KEY);
// ✅ 正确做法
fetch('/api/weather?city=Beijing');
// 服务器端转发请求,添加密钥
6. 节流和防抖
节流(Throttle):限制函数执行频率
function throttle(func, delay) {
let lastCall = 0;
return function(...args) {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
return func.apply(this, args);
};
}
// 使用示例:限制搜索频率
const throttledSearch = throttle((city) => {
getWeather(city);
}, 1000); // 最少间隔1秒
防抖(Debounce):延迟执行,只在最后一次触发后执行
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用示例:输入停止后才搜索
const debouncedSearch = debounce((city) => {
if (city) getWeather(city);
}, 500); // 停止输入500ms后执行
✍️ 练习任务
基础练习
-
练习fetch请求
- 使用JSONPlaceholder API练习GET请求
- 尝试获取用户列表、文章列表
- 显示获取的数据
-
错误处理练习
- 故意输入错误的URL
- 实现完整的错误处理逻辑
- 显示友好的错误提示
进阶练习
-
实现节流和防抖
- 为搜索框添加防抖功能
- 优化用户体验,减少API请求
-
添加加载状态
- 在请求期间显示加载动画
- 禁用搜索按钮防止重复点击
实战练习
-
扩展天气应用
- 添加未来5天天气预报
- 显示天气图表(温度变化趋势)
- 支持多城市对比
-
创建其他API应用
- GitHub用户搜索
- 电影信息查询(TMDB API)
- 新闻阅读器
🎓 今日挑战
挑战任务:增强版天气应用
需求:
- 支持用户当前位置自动定位
- 添加多城市管理(保存偏好城市)
- 实现天气预警功能
- 添加天气分享功能(生成分享链接)
提示:
- 使用Geolocation API获取当前位置
- 使用localStorage保存城市列表
- 查阅天气API文档获取预警数据
💡 常见问题 FAQ
Q1: fetch和axios有什么区别?
Fetch API:
- 原生浏览器API,无需安装
- 基于Promise设计
- 需要手动处理某些细节(如超时、取消)
Axios:
- 第三方库,需要引入
- 自动转换JSON数据
- 内置请求/响应拦截器
- 更好的错误处理
- 支持请求超时配置
选择建议:
- 小项目:使用fetch(轻量)
- 大项目:使用axios(功能完善)
Q2: 如何处理文件上传?
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
formData.append('description', 'My file');
fetch('https://api.example.com/upload', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => console.log('上传成功:', data));
Q3: 如何设置请求超时?
参见上面的"请求超时处理"章节,使用Promise.race()实现。
Q4: 如何并行发送多个请求?
// Promise.all - 所有请求都成功
Promise.all([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
fetch('https://api.example.com/comments'),
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([users, posts, comments]) => {
console.log('用户:', users);
console.log('文章:', posts);
console.log('评论:', comments);
});
// Promise.allSettled - 等待所有请求完成(不管成功或失败)
Promise.allSettled([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
])
.then(results => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
console.log(`请求${i + 1}成功:`, result.value);
} else {
console.error(`请求${i + 1}失败:`, result.reason);
}
});
});
📚 拓展阅读
推荐资源
-
MDN – Fetch API:
https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API -
HTTP协议详解:
https://developer.mozilla.org/zh-CN/docs/Web/HTTP -
RESTful API设计指南:
https://restfulapi.net/ -
免费API资源:
- JSONPlaceholder: https://jsonplaceholder.typicode.com/
- OpenWeatherMap: https://openweathermap.org/api
- GitHub API: https://docs.github.com/en/rest
下一步学习
- Day 30: 项目6-天气查询应用(完整实现)
- async/await深入
- WebSocket实时通信
- GraphQL API
下一步学习: Day 30: 项目6-天气查询应用