第6章 6.3 Fetch API 与 AJAX:让网页学会「打电话」
全文约 5000 字 | 90 分钟学习节奏 | 难度:进阶
🎯 开场 3 分钟:为什么要学这个?
你有没有遇到过这种情况:刷朋友圈时,往下拉自动加载新内容,页面不用刷新新帖子就出来了;或者填了个表单点提交,整个页面闪一下结果内容已经存好了。
这背后,就是 AJAX 在干活。
AJAX = Asynchronous JavaScript And XML(异步 JavaScript 和 XML)。说人话:网页偷偷跟服务器「打电话」,不打断你当前看的内容,电话打完数据就自动更新上来了。
上一章我们学了事件处理——用户点个按钮,网页能响应了。但网页还不会主动「打电话」,只能等着用户操作。
这一章学完,你能做出:
- 自动刷新天气的「小组件」
- 填完表单不跳转页面就提交成功
- 滚动到底部自动加载下一页内容
你的网页,从此有了「打电话」的能力。
🧱 基础 25 分钟:核心概念
什么是 F\n\n
\n\n
\n\netch API?
生活类比:
想象你点外卖。你跟店家说「我要一份宫保鸡丁」,店家后厨开始做,你不用站在店门口傻等,可以继续刷手机。做完了外卖小哥把饭送过来。
Fetch API 就是网页的「点外卖系统」——告诉它要什么数据,它去服务器要,你不用卡住界面,拿到数据再处理。
为什么要用?
老式网页是这样的:点个链接,整页刷新,服务器返回全新 HTML,你盯着白屏等 3 秒。
用了 Fetch API:你点按钮,网页偷偷发请求,拿到数据后只更新页面的一小块,其他内容纹丝不动。
怎么用?
// 最简单的例子:获取一张图片
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json()) // 把服务器返回的东西转成 JS 能用的格式
.then(data => console.log(data)) // 打印出来看看
.catch(error => console.log('出错了:', error)) // 兜底,万一网络炸了呢
运行后输出大概是这样:
{
userId: 1,
id: 1,
title: "sunt aut facere repellat provident",
body: "quia et suscipit\nsuscipit recusandae consequuntur..."
}
解释一下:.then() 是「等请求成功后干嘛」,.catch() 是「出问题了怎么办」。
Response 对象:服务器的「回信」
上一章我们处理事件,事件有个 event 对象。Fetch 发请求后,服务器回的内容包装在 Response 对象里。
生活类比:
你寄信出去,收到回信。信封上有邮编、发件人、是否超时、是否损坏——这些是信封的信息。信纸里面的内容,才是真正的数据。
Response 对象就是那个「信封」,你得先检查信封正不正常,再拆开看内容。
为什么要用?
不是所有请求都能成功——网络断了、服务器挂了、地址写错了。Response 对象里有 ok、status 这些属性,告诉你请求到底成没成。
怎么用?
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => {
console.log('状态码:', response.status) // 200 表示成功,404 表示没找到
console.log('是否成功:', response.ok) // true 或 false
return response.json() // 解析数据,记得 return 出去
})
.then(data => console.log(data))
async/await:让异步代码看起来像「同步」的
前面用的 .then() 链,写多了会变成「回调地狱」:
fetch(url)
.then(r => r.json())
.then(data => {
// 嵌套多了代码往右歪
})
async/await 就是来解决这个问题的,让代码像读文章一样从上往下读。
生活类比:
.then() 像是点外卖时跟服务员说「做好了叫我」,然后你去干别的。等做好了叫你,你再去处理。
await 像是你站在取餐口等,服务员说「稍等 3 分钟」,你就站在那等,不干别的,3 分钟后直接拿到饭。
为什么要用?
代码更好看,更容易理解,也更好调试。
怎么用?
// 用 async/await 重写前面的例子
async function getPost() {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
if (!response.ok) {
throw new Error(`HTTP 错误,状态码: ${response.status}`)
}
const data = await response.json()
console.log(data)
} catch (error) {
console.log('获取失败:', error)
}
}
getPost()
注意:
- async 放在 function 前面,表示这个函数里有 await
- await 只能用在 async 函数里
- try...catch 兜底,跟 .then().catch() 一个意思
Request 对象:精细化控制请求
有时候你不仅要 GET 数据,还要 POST 数据、上传文件、发送自定义 Header。
fetch() 的第二个参数就是 Request 对象,告诉你怎么「发货」。
生活类比:
你点外卖,店家问你要不要开发票、要不要备注辣度。fetch() 的第二个参数就是这些额外要求。
怎么用?
async function createPost() {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST', // 告诉服务器我要「新建」数据
headers: {
'Content-Type': 'application/json' // 告诉服务器我发的是 JSON 格式
},
body: JSON.stringify({ // 把 JS 对象转成 JSON 字符串
title: '我的第一篇文章',
body: '这是内容',
userId: 1
})
})
const data = await response.json()
console.log('创建成功:', data)
}
createPost()
运行后输出:
{
title: "我的第一篇文章",
body: "这是内容",
userId: 1,
id: 101 // 服务器新分配了一个 ID
}
AbortController:取消请求
请求发出去后,有时候用户突然关页面、或者点了取消,需要把请求掐掉。
生活类比:
你点了外卖,突然不想吃了,打电话给店家「别做了」。AbortController 就是那个「取消电话」。
怎么用?
const controller = new AbortController()
async function fetchWithCancel() {
// 2 秒后自动取消
const timeout = setTimeout(() => controller.abort(), 2000)
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1', {
signal: controller.signal // 把「取消信号」注册到请求里
})
const data = await response.json()
console.log(data)
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求被取消了')
} else {
console.log('出错了:', error)
}
}
}
fetchWithCancel()
如果网络够快,你会正常拿到数据。如果超过 2 秒,会打印「请求被取消了」。
上传/下载进度:让用户知道进度
传大文件时,用户盯着空白进度条会很慌。需要实时告诉用户「已经传了 30%」。
生活类比:
安装软件时那个进度条,告诉你「已完成 45/100 步」。上传下载进度同理。
怎么用?
// 注意:这个需要你的服务器支持 Range 请求,这里演示原理
async function downloadWithProgress(url, onProgress) {
const response = await fetch(url)
const reader = response.body.getReader() // 读取流
const contentLength = response.headers.get('Content-Length') // 总大小
let received = 0
while (true) {
const { done, value } = await reader.read()
if (done) break
received += value.length
const percent = Math.round((received / contentLength) * 100)
onProgress(percent) // 回调进度
}
}
// 用法
downloadWithProgress(
'https://jsonplaceholder.typicode.com/posts',
(percent) => console.log(`下载进度: ${percent}%`)
)
注意:浏览器原生 Fetch API 的进度监听比较复杂,实际项目中常用 XMLHttpRequest 或者第三方库(如 axios)来做进度监控。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):天气查询小工具
需求: 输入城市名,显示天气(用免费的 JSONPlaceholder 模拟)
// 完整的天气查询工具
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>天气查询</title>
</head>
<body>
<h1>天气查询</h1>
<input type="text" id="cityInput" placeholder="输入城市名">
<button id="searchBtn">查询</button>
<div id="result"></div>
<script>
const cityInput = document.getElementById('cityInput')
const searchBtn = document.getElementById('searchBtn')
const resultDiv = document.getElementById('result')
searchBtn.addEventListener('click', async () => {
const city = cityInput.value.trim()
if (!city) {
resultDiv.innerHTML = '请输入城市名'
return
}
resultDiv.innerHTML = '加载中...'
try {
// 用 JSONPlaceholder 模拟 API 调用
const response = await fetch(`https://jsonplaceholder.typicode.com/users?city=${city}`)
if (!response.ok) {
throw new Error('网络请求失败')
}
const users = await response.json()
if (users.length === 0) {
resultDiv.innerHTML = `找不到城市「${city}」的天气数据`
} else {
// 模拟天气数据
resultDiv.innerHTML = `
<h3>${city}的天气</h3>
<p>温度:${Math.round(Math.random() * 15 + 10)}°C</p>
<p>天气:${['晴', '多云', '小雨'][Math.floor(Math.random() * 3)]}</p>
<p>数据来源:模拟数据(真实接口需要 API Key)</p>
`
}
} catch (error) {
resultDiv.innerHTML = `查询失败:${error.message}`
}
})
</script>
</body>
</html>
预期输出: 输入「北京」点查询,显示北京的气温、天气情况。
一句话解释: 用 fetch() 发请求,用 async/await 等待结果,用 try...catch 兜底处理错误。
项目 2(15 分钟):博客文章列表 + 分页加载
需求: 从 API 获取文章列表,点击「加载更多」下一页,滚动到底部也自动加载。
// 博客文章列表 + 分页
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>博客文章列表</title>
<style>
.post { border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px; }
.loading { text-align: center; color: #666; padding: 20px; }
.hidden { display: none; }
</style>
</head>
<body>
<h1>博客文章列表</h1>
<div id="posts"></div>
<div id="loading" class="loading hidden">加载中...</div>
<button id="loadMore" class="hidden">加载更多</button>
<script>
const postsContainer = document.getElementById('posts')
const loadingDiv = document.getElementById('loading')
const loadMoreBtn = document.getElementById('loadMore')
let page = 1
const limit = 10
let isLoading = false
let hasMore = true
// 获取文章列表
async function fetchPosts(pageNum, limitNum) {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_page=${pageNum}&_limit=${limitNum}`
)
const data = await response.json()
const totalPosts = response.headers.get('X-Total-Count')
return { data, totalPosts }
}
// 渲染单篇文章
function renderPost(post) {
const div = document.createElement('div')
div.className = 'post'
div.innerHTML = `
<h3>${post.id}. ${post.title}</h3>
<p>${post.body}</p>
`
postsContainer.appendChild(div)
}
// 加载并渲染
async function loadPosts() {
if (isLoading || !hasMore) return
isLoading = true
loadingDiv.classList.remove('hidden')
loadMoreBtn.classList.add('hidden')
try {
// 模拟网络延迟,让进度更明显
await new Promise(resolve => setTimeout(resolve, 800))
const { data, totalPosts } = await fetchPosts(page, limit)
data.forEach(post => renderPost(post))
// 判断是否还有更多
const loadedCount = page * limit
hasMore = loadedCount < 100 // JSONPlaceholder 总共 100 条
page++
if (hasMore) {
loadMoreBtn.classList.remove('hidden')
}
} catch (error) {
console.error('加载失败:', error)
postsContainer.innerHTML += '<p style="color:red">加载失败,请刷新重试</p>'
} finally {
isLoading = false
loadingDiv.classList.add('hidden')
}
}
// 绑定按钮事件
loadMoreBtn.addEventListener('click', loadPosts)
// 滚动到底部自动加载
window.addEventListener('scroll', () => {
const scrollBottom = window.innerHeight + window.scrollY
const documentHeight = document.body.offsetHeight
if (scrollBottom >= documentHeight - 100) {
loadPosts()
}
})
// 初始加载
loadPosts()
</script>
</body>
</html>
预期输出: 页面加载后显示 10 篇文章,滚动到底部自动加载下一页,或者点「加载更多」按钮。
一句话解释: 用 ?_page=&_limit= 参数做分页,用 response.headers.get('X-Total-Count') 知道总共多少条,用 scroll 事件判断是否滚到底。
项目 3(15 分钟):待办事项管理器(增删改查)
需求: 从 JSONPlaceholder 获取待办数据,本地添加、删除、标记完成,数据变更发请求同步到「服务器」。
// 待办事项管理器
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>待办事项</title>
<style>
body { font-family: sans-serif; max-width: 500px; margin: 40px auto; padding: 0 20px; }
.todo-item { display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee; }
.todo-item.completed span { text-decoration: line-through; color: #999; }
.todo-item input[type="checkbox"] { margin-right: 10px; transform: scale(1.5); }
.delete-btn { margin-left: auto; color: #ff4444; cursor: pointer; background: none; border: none; font-size: 16px; }
#addForm { display: flex; gap: 10px; margin-bottom: 20px; }
#addForm input { flex: 1; padding: 10px; font-size: 16px; }
#addForm button { padding: 10px 20px; font-size: 16px; background: #4CAF50; color: white; border: none; cursor: pointer; }
</style>
</head>
<body>
<h1>我的待办</h1>
<form id="addForm">
<input type="text" id="newTodo" placeholder="输入新待办..." required>
<button type="submit">添加</button>
</form>
<div id="todoList"></div>
<p id="status"></p>
<script>
const todoList = document.getElementById('todoList')
const addForm = document.getElementById('addForm')
const newTodoInput = document.getElementById('newTodo')
const statusP = document.getElementById('status')
let todos = []
// 从 API 加载待办
async function loadTodos() {
statusP.textContent = '加载中...'
try {
const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=10')
todos = await response.json()
renderTodos()
statusP.textContent = ''
} catch (error) {
statusP.textContent = '加载失败: ' + error.message
}
}
// 渲染待办列表
function renderTodos() {
todoList.innerHTML = ''
todos.forEach(todo => {
const div = document.createElement('div')
div.className = 'todo-item' + (todo.completed ? ' completed' : '')
div.innerHTML = `
<input type="checkbox" ${todo.completed ? 'checked' : ''}
onchange="toggleTodo(${todo.id})">
<span>${todo.title}</span>
<button class="delete-btn" onclick="deleteTodo(${todo.id})">×</button>
`
todoList.appendChild(div)
})
}
// 添加待办(本地 + 模拟 POST)
async function addTodo(title) {
try {
// 模拟 POST 请求(JSONPlaceholder 不会真正保存)
const response = await fetch('https://jsonplaceholder.typicode.com/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, completed: false, userId: 1 })
})
const newTodo = await response.json()
// 本地添加(JSONPlaceholder 返回的 id 是 201,不是真正新增的)
todos.unshift({ id: Date.now(), title: newTodo.title, completed: false })
renderTodos()
} catch (error) {
alert('添加失败: ' + error.message)
}
}
// 切换完成状态
function toggleTodo(id) {
const todo = todos.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
renderTodos()
// 实际项目这里会发 PATCH/PUT 请求
}
}
// 删除待办
function deleteTodo(id) {
todos = todos.filter(t => t.id !== id)
renderTodos()
// 实际项目这里会发 DELETE 请求
}
// 表单提交
addForm.addEventListener('submit', async (e) => {
e.preventDefault()
const title = newTodoInput.value.trim()
if (title) {
await addTodo(title)
newTodoInput.value = ''
}
})
// 初始化
loadTodos()
</script>
</body>
</html>
预期输出: 显示 10 条待办,能勾选完成、点击 × 删除、在顶部输入框添加新待办。
一句话解释: CRUD(增删改查)是最常见的 Web 操作,本项目演示了如何用 fetch() 配合不同 method(GET/POST)完成这些操作。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:忘记 return response.json()
// ❌ 错误:没有 return,数据流断了
fetch(url)
.then(response => response.json()) // 这里没 return!
.then(data => console.log(data)) // data 是 undefined
// ✅ 正确:每一级都 return 出去
fetch(url)
.then(response => {
return response.json() // return 出去才能到下一个 .then
})
.then(data => console.log(data))
坑 2:用 .then() 但没加 .catch()
// ❌ 错误:请求失败时没有任何提示
fetch(url)
.then(response => response.json())
.then(data => console.log(data))
// 如果网络断了,浏览器控制台会报 Uncaught (in promise) undefined
// ✅ 正确:永远加 .catch() 或者用 try...catch
fetch(url)
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log('请求失败:', error))
// 或者用 async/await
async function getData() {
try {
const response = await fetch(url)
const data = await response.json()
} catch (error) {
console.log('请求失败:', error)
}
}
坑 3:fetch 默认不带 Cookie
前后端分离项目如果用到 Session 或 JWT:
// ❌ 错误:默认不带 Cookie,登录状态丢失
fetch('/api/user')
// ✅ 正确:手动加上 credentials
fetch('/api/user', {
credentials: 'include' // 带上 Cookie
})
坑 4:response.ok 不一定是 true
// ❌ 错误:以为 status 200 才是成功
const response = await fetch(url)
if (response.status === 200) { // 404、500 不会进入这里
// ...
}
// ✅ 正确:用 response.ok 判断
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
} else {
console.log(`请求失败,状态码: ${response.status}`)
}
坑 5:JSON.parse 放在 await 之后
// ❌ 错误:response.json() 返回的是 Promise,要 await
const data = response.json() // 返回 Promise,不是数据!
// ✅ 正确:await 获取 Promise 的结果
const data = await response.json()
性能小贴士:避免重复请求
同一个接口,短时间被多次触发(比如用户手抖连点了两次按钮),会浪费带宽。
// 用 AbortController 取消上一次的请求
let currentController = null
async function fetchData() {
// 取消上一次
if (currentController) {
currentController.abort()
}
currentController = new AbortController()
try {
const response = await fetch(url, {
signal: currentController.signal
})
const data = await response.json()
console.log(data)
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求被取消') // 这是正常的,不算报错
} else {
console.error('出错了:', error)
}
}
}
调试技巧:用 F12 Network 面板
浏览器按 F12 打开开发者工具,切到 Network 标签页,能看到:
- 请求的 URL、Method、Status
- 请求头(Headers)和响应体(Response)
- 请求耗时
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):换个接口
// 下面代码获取的是文章,改为获取用户信息
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data))
- 提示:把
posts/1改成users/1
练习 2(2 分钟):加个判断
// 给下面的代码加上状态码判断
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data))
- 提示:用
if (!response.ok) throw new Error(...)
练习 3(3 分钟):POST 数据
// 用 fetch POST 一条新数据
// 接口:https://jsonplaceholder.typicode.com/posts
// 要发送:{ title: "测试", body: "内容", userId: 1 }
- 提示:需要 method、headers、body 三个参数
练习 4(3 分钟):改成 async/await
// 把下面改成 async/await 写法
function getPost() {
return fetch('https://jsonplaceholder.typicode.com/posts/1')
.then(response => response.json())
.then(data => console.log(data))
}
- 提示:function 前加 async,里面用 await
练习 5(5 分钟):分析报错
Uncaught (in promise) TypeError: Failed to fetch
- 问题:可能是什么原因导致的?
- 提示:考虑网络层面和代码层面的原因
作业题(30 分钟 - 2 小时)
做一个「随机英文单词卡」
需求描述: 每次点按钮显示一个随机英文单词及其释义,帮助背单词。
功能点:
1. 调用 https://random-word-api.herokuapp.com/word 获取随机单词
2. 用 https://api.dictionaryapi.dev/api/v2/entries/en/单词 查询释义
3. 点按钮换下一个单词
4. 显示「认识」「不认识」两个按钮,点的次数要统计
加分项:
1. 加个进度条,显示今天背了多少词
2. 把不认识的词存到 localStorage,下次复习
验收标准:
- 能跑起来,不报错
- 点按钮能显示单词和释义
- 计数器正确累加
📚 总结 + 资源
3 句话总结:
- Fetch API 让网页能「打电话」给服务器,不用刷新页面就能获取或发送数据
async/await让异步代码写起来像同步代码,更容易阅读和调试- 请求可能失败,永远记得加
.catch()或try...catch兜底
延伸资源:
- MDN Fetch API 文档 — 权威、全面,入门后值得细读
- 《JavaScript 高级程序设计》- 红宝书第 12 章 — 系统学习 AJAX 和 Fetch
- JSONPlaceholder — 免费练习接口,拿来就能用
互动钩子:
你在实际项目中用过 Fetch 吗?遇到过什么坑?是跟后端调试 CORS 问题还是被 JSON 格式坑过?评论区聊聊,老粉优先回复!
下章预告:
学会了「打电话」,那网页「知道」自己在哪吗?浏览器自带的 BOM(Browser Object Model)能告诉你当前网址是什么、用户用的是什么浏览器、甚至能跳转到新页面……下一章我们聊聊这些「浏览器自带的功能」。
📍 下一章预告:「第 6 章 6.4 BOM:window/location/navigator」— 浏览器那些「知道但不会用」的功能

评论(0)