第6章 6.3 Fetch API 与 AJAX:让网页学会「打电话」

全文约 5000 字 | 90 分钟学习节奏 | 难度:进阶


🎯 开场 3 分钟:为什么要学这个?

你有没有遇到过这种情况:刷朋友圈时,往下拉自动加载新内容,页面不用刷新新帖子就出来了;或者填了个表单点提交,整个页面闪一下结果内容已经存好了。

这背后,就是 AJAX 在干活。

AJAX = Asynchronous JavaScript And XML(异步 JavaScript 和 XML)。说人话:网页偷偷跟服务器「打电话」,不打断你当前看的内容,电话打完数据就自动更新上来了。

上一章我们学了事件处理——用户点个按钮,网页能响应了。但网页还不会主动「打电话」,只能等着用户操作。

这一章学完,你能做出:
- 自动刷新天气的「小组件」
- 填完表单不跳转页面就提交成功
- 滚动到底部自动加载下一页内容

你的网页,从此有了「打电话」的能力。


🧱 基础 25 分钟:核心概念

什么是 F\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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 对象里有 okstatus 这些属性,告诉你请求到底成没成。

怎么用?

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 句话总结:

  1. Fetch API 让网页能「打电话」给服务器,不用刷新页面就能获取或发送数据
  2. async/await 让异步代码写起来像同步代码,更容易阅读和调试
  3. 请求可能失败,永远记得加 .catch()try...catch 兜底

延伸资源:

  1. MDN Fetch API 文档 — 权威、全面,入门后值得细读
  2. 《JavaScript 高级程序设计》- 红宝书第 12 章 — 系统学习 AJAX 和 Fetch
  3. JSONPlaceholder — 免费练习接口,拿来就能用

互动钩子:

你在实际项目中用过 Fetch 吗?遇到过什么坑?是跟后端调试 CORS 问题还是被 JSON 格式坑过?评论区聊聊,老粉优先回复!


下章预告:

学会了「打电话」,那网页「知道」自己在哪吗?浏览器自带的 BOM(Browser Object Model)能告诉你当前网址是什么、用户用的是什么浏览器、甚至能跳转到新页面……下一章我们聊聊这些「浏览器自带的功能」。

📍 下一章预告:「第 6 章 6.4 BOM:window/location/navigator」— 浏览器那些「知道但不会用」的功能

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。