Ajax和异步请求
Ajax和异步请求
为什么需要Ajax
到目前为止,我们写过的大多数页面逻辑,都发生在当前这张页面里:
- 读取用户输入。
- 修改当前 DOM。
- 让页面看起来“动起来了”。
这当然很重要,但真实网站还有一个更麻烦的问题:页面上展示的数据,往往不在当前 html 文件里,而是在服务器、数据库,或者某个 json 接口里。
如果没有 Ajax,那么每次你想拿到新数据,都只能重新请求一整张新页面。这样会带来几个直接问题:
- 页面要整体刷新,用户当前的滚动位置、输入状态、临时计算结果都可能丢失。
- 明明只想更新页面中的一小块内容,却要把整张页面重新下载一遍,浪费带宽。
- 页面等待服务器返回时,交互会变得很笨重,体验很差。
- 前端只能“收整页、换整页”,很难做出搜索联想、局部刷新、无刷新保存这类效果。
所以我们需要一种能力:让 JavaScript 在不刷新整张页面的前提下,主动请求数据,然后只更新需要变化的那一部分 DOM。
这就是 Ajax 的意义。
Ajax 这个词为什么看起来有点老?
Ajax 是 Asynchronous JavaScript and XML 的缩写。这个名字保留了历史痕迹,但今天实际开发里,返回的数据更多是 json,而不是 xml。 所以你要抓住它真正的核心:异步请求 + JavaScript 更新页面。
下面这两条世界线的区别,正好能说明 Ajax 的价值:
像“加载更多”“刷新排行榜”“搜索建议”“聊天记录滚动加载”“提交后局部更新结果”这些效果,背后基本都离不开这一套思路。
用 axios 请求静态 json 并刷新页面
原生的 XMLHttpRequest 可以完成 Ajax,但写法比较繁琐。实际开发里通常会使用封装好的请求库,例如 axios。
对我们当前这个阶段来说,最容易上手的方式是直接在页面里通过 CDN 引入:
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
注意这行 script 要放在你自己写的请求代码前面,否则浏览器还没加载到 axios,你就开始调用它了。
准备一个静态 json 文件
假设当前目录下有一个 books.json:
[
{
"title": "HTML 入门",
"author": "张三",
"price": 39
},
{
"title": "CSS 页面设计",
"author": "李四",
"price": 45
},
{
"title": "JavaScript 交互",
"author": "王五",
"price": 52
}
]
配套示例资源建议怎么放
为了让你在本地测试时不容易把路径写乱,建议把本章示例整理成下面这种结构:
ajax-demo/
index.html
books.json
如果你想把结构稍微分清楚一点,也可以这样放:
ajax-demo/
index.html
data/
books.json
js/
index.js
这两种都可以,关键是你要清楚请求路径是相对于当前页面地址来计算的。
例如:
- 如果
index.html和books.json在同一级目录,那么请求可以写成./books.json。 - 如果
books.json放在data/目录里,那么请求应该写成./data/books.json。 - 如果你把自己的脚本拆到了
js/index.js,Ajax 请求路径也仍然是相对页面地址,而不是相对 js 文件地址。
初学者最常见的 404,往往不是 axios 不会用,而是目录和相对路径没有对应好。
你也可以直接打开本章配套的可运行示例,对照看目录结构和请求路径:
页面代码
我们希望页面加载时自动请求一次数据,点击按钮时还能再次刷新列表:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Ajax Demo</title>
</head>
<body>
<h1>图书列表</h1>
<button id="reload-btn">重新加载</button>
<p id="status">尚未加载</p>
<ul id="book-list"></ul>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const statusEl = document.getElementById("status");
const listEl = document.getElementById("book-list");
const reloadBtn = document.getElementById("reload-btn");
function renderBooks(books) {
listEl.innerHTML = "";
for (const book of books) {
const li = document.createElement("li");
li.innerText = `${book.title} - ${book.author} - ¥${book.price}`;
listEl.appendChild(li);
}
}
async function loadBooks() {
statusEl.innerText = "正在加载...";
try {
const resp = await axios.get("./books.json");
renderBooks(resp.data);
statusEl.innerText = `已加载 ${resp.data.length} 条数据`;
} catch (error) {
console.error(error);
statusEl.innerText = "加载失败,请按 F12 查看错误";
}
}
reloadBtn.addEventListener("click", loadBooks);
window.addEventListener("load", loadBooks);
</script>
</body>
</html>
这段代码做了几件事:
- 页面加载完成后,调用
loadBooks()发送请求。 - 请求成功时,从
resp.data里取出 json 数据。 - 调用
renderBooks(),把数据渲染到页面的ul中。 - 请求失败时,把错误打印到控制台,同时在页面上给出提示。
这里最重要的一点是:页面刷新的不是浏览器窗口,而是我们自己选中的那一块 DOM。
为什么这个例子很有代表性
这个例子虽然简单,但它已经覆盖了 Ajax 的核心流程:
- 页面事件触发请求。
- 请求返回数据。
- JavaScript 解析数据。
- JavaScript 根据数据更新 DOM。
把这个模型理解透,后面的搜索框联想、商品列表分页、用户信息加载,本质上都是同一件事,只是数据和页面结构更复杂而已。
不要直接双击 html 文件测试
如果你直接用 file:/// 的方式打开 html,Ajax 请求经常会因为协议或路径问题失败。
请用我们前面说过的 Live Server,或者任何本地 HTTP 服务来测试。对你来说,最简单的方式仍然是:
- 用 VS Code 打开目录。
- 启动 Live Server。
- 在浏览器里打开页面并按 F12 调试。
axios 里怎么传参数和 json 请求体
前面的例子里,我们只是最简单地读取了一个静态 json 文件。但真实开发里,请求通常还会带上额外信息,例如:
- 搜索关键字。
- 页码和每页条数。
- 要提交给服务器的新数据。
这里最容易混淆的是两类“传值方式”:
- URL 参数:会出现在地址栏的
?后面。 - 请求体:不会拼到 URL 后面,而是放在请求正文里发送。
你可以先把它们理解成一句话:
查数据时,常见的是用 URL 参数;提交一整份对象数据时,常见的是用请求体。
GET 请求里传查询参数
如果你想按条件筛选列表,通常会把参数写进 params 里:
async function searchBooks() {
try {
const resp = await axios.get("/api/books", {
params: {
keyword: "JavaScript",
page: 2,
pageSize: 5
}
});
console.log(resp.data);
} catch (error) {
console.error(error);
}
}
这段代码发出去之后,请求地址通常会变成类似这样:
/api/books?keyword=JavaScript&page=2&pageSize=5
也就是说,下面这段配置:
{
params: {
keyword: "JavaScript",
page: 2,
pageSize: 5
}
}
本质上是在告诉 axios:
请帮我把这些字段拼成 URL 查询参数。
这种写法特别适合下面这些场景:
- 搜索。
- 分页。
- 排序。
- 根据 id 或分类过滤列表。
如果你在浏览器的 Network 面板里看这类请求,最直观的特征就是:参数会直接出现在 Request URL 上。
POST 请求里传 json 请求体
如果你不是“查列表”,而是要“提交一份数据”给服务器,比如新增图书、保存表单、提交评论,通常会把对象放进请求体里:
async function createBook() {
try {
const resp = await axios.post("/api/books", {
title: "JavaScript 交互",
author: "王五",
price: 52,
inStock: true
});
console.log(resp.data);
} catch (error) {
console.error(error);
}
}
这里第二个参数不是 params 配置,而是真正要提交给服务器的那份数据对象。
axios 遇到普通对象时,通常会把它当成 json 发送。发送出去的请求体大致相当于:
{
"title": "JavaScript 交互",
"author": "王五",
"price": 52,
"inStock": true
}
所以你要记住一个很重要的区别:
params是给 URL 用的。post的第二个参数,常常就是请求体数据。
参数、配置、请求体三者怎么区分
很多初学者一看到 axios 的括号里能放很多东西,就容易混乱。可以先记这两个最常见的函数签名:
axios.get(url, config)
axios.post(url, data, config)
对应到实际代码里就是:
axios.get("/api/books", {
params: {
page: 1
}
});
axios.post(
"/api/books",
{
title: "新书"
},
{
headers: {
"Content-Type": "application/json"
}
}
);
这里要分清:
url是请求地址。data是请求体。config是额外配置,例如params、headers、超时时间等。
如果你把 params 和 data 的位置放错,请求虽然能发出去,但服务器收到的内容就可能不是你以为的那样。
一个同时包含两种传值方式的例子
有些请求会同时出现“地址上的条件”和“请求体里的数据”。例如你要修改 id 为 12 的图书,并且在 URL 上带一个调试参数:
async function updateBook() {
try {
const resp = await axios.put(
"/api/books/12",
{
title: "JavaScript 高级交互",
price: 68
},
{
params: {
debug: true
}
}
);
console.log(resp.data);
} catch (error) {
console.error(error);
}
}
这时:
- URL 里会有
?debug=true。 - 请求体里会有要更新的那份
json数据。
所以不要把“请求带参数”只理解成一种形式。参数可能在 URL 上,请求数据也可能在 body 里,它们是两层不同的东西。
调试时应该看哪里
如果你怀疑自己把参数传错了,不要靠猜,直接去 F12 里看:
- 看
Headers里的Request URL,确认查询参数有没有真的拼上去。 - 看
Payload或Request Payload,确认json请求体是不是你想提交的内容。 - 看
Response,确认服务器返回的是成功信息,还是参数错误提示。
很多“我明明传了,为什么后端收不到”的问题,最后都不是 axios 出错,而是:
- 你把该放在
params的内容写进了data。 - 你把该放在请求体里的对象错写成了查询字符串。
- 你以为自己发的是
POST,其实代码里写成了GET。
异步请求的时序和处理逻辑
Ajax 最容易让初学者犯错的地方,不是语法,而是异步。
一个非常常见的错误
很多人第一次写 Ajax,会下意识地把它当成同步代码:
let books = [];
function loadBooksWrong() {
axios.get("./books.json").then(function(resp) {
books = resp.data;
});
console.log(books);
}
初学者往往以为 console.log(books) 会打印出请求回来的数据,但实际上这里大概率只会看到空数组。
原因不是 axios 坏了,而是你的思维还停留在“写在下面的代码一定后执行完”的同步世界里。
真正发生的时序
下面这张图更接近真实情况:
也就是说:
- 请求发出去之后,浏览器不会傻等着。
- 主线程会先继续执行后面的同步代码。
- 等网络结果真的回来了,再执行
then(...)或await后面的那段逻辑。
所以在 Ajax 代码里,一个非常重要的原则是:
依赖请求结果的逻辑,必须写在请求成功之后。
例如渲染页面、修改状态、显示成功提示,都应该放在 then 回调里,或者放在 await 之后。
then 写法和 async/await 写法
下面这两种写法,本质上是在表达同一件事。
先看 then:
function loadBooks() {
axios.get("./books.json")
.then(function(resp) {
renderBooks(resp.data);
})
.catch(function(error) {
console.error(error);
});
}
再看 async/await:
async function loadBooks() {
try {
const resp = await axios.get("./books.json");
renderBooks(resp.data);
} catch (error) {
console.error(error);
}
}
后者更像“按顺序写代码”,可读性通常更好,但你要牢牢记住一件事:
await 并没有把异步请求变成同步请求,它只是让你能用更接近同步的写法来描述异步流程。
换句话说,async/await 解决的是“代码难读”的问题,不是“网络很慢”的问题。
this 为什么经常写错
Ajax 里另一个高频坑是 this。
先看一个常见错误:
const page = {
books: [],
load: function() {
axios.get("./books.json").then(function(resp) {
this.books = resp.data;
this.render();
});
},
render: function() {
console.log(this.books);
}
};
很多同学会认为 this 一定指向 page。但在 then(function(resp) { ... }) 这个普通函数里,this 并不是你想象中的那个对象,因此 this.books 和 this.render() 很容易出问题。
对于初学者来说,有两种最常用的处理办法。
第一种,用箭头函数:
const page = {
books: [],
load: function() {
axios.get("./books.json").then((resp) => {
this.books = resp.data;
this.render();
});
},
render: function() {
console.log(this.books);
}
};
箭头函数不会自己创建新的 this,它会沿用外层 load 方法里的 this,因此这里就更容易得到我们想要的效果。
第二种,提前把 this 保存下来:
const page = {
books: [],
load: function() {
const self = this;
axios.get("./books.json").then(function(resp) {
self.books = resp.data;
self.render();
});
},
render: function() {
console.log(this.books);
}
};
这两种写法都很常见。对于我们现在这个阶段,我更建议你优先理解:
- 普通函数里的
this不一定是你想的那个对象。 - 箭头函数会继承外层的
this。 - 如果代码本来就不复杂,甚至可以少用
this,直接用普通函数和局部变量。
一个更完整的对象写法
把 this 和 async 放在一起看,会更清楚:
const page = {
books: [],
async load() {
try {
const resp = await axios.get("./books.json");
this.books = resp.data;
this.render();
} catch (error) {
console.error(error);
}
},
render() {
console.log(this.books);
}
};
page.load();
这里的处理逻辑非常清晰:
load()发出请求。await等待这一次请求的结果。- 请求成功后,把数据写入对象状态
this.books。 - 立刻调用
render()刷新页面。
这也是你今后做前端页面时最常见的套路之一:
请求数据 -> 写入状态 -> 刷新视图
如何测试和调试 Ajax
Ajax 出问题时,不要只盯着代码发呆。前端调试最有价值的工具依旧是浏览器 F12。
先看 Network 面板
按 F12 打开开发者工具,切到 Network 面板,再重新触发你的请求。你至少应该确认这几件事:
- 请求到底有没有真的发出去。
- 请求的 URL 对不对。
- 返回状态码是不是
200。 - 返回的数据到底是什么。
点开某条请求后,可以重点看:
Headers:请求地址、请求方法、状态码。Preview:浏览器帮你预览返回的数据。Response:服务器真正返回的原始内容。
如果你明明觉得“代码写对了”,却还是没有数据显示,那么第一反应就应该是:先去 Network 看看到底请求到了什么。
再看 Console 面板
控制台最适合看两类信息:
- 你自己打印的调试信息。
- 浏览器抛出的错误信息。
例如:
async function loadBooks() {
try {
const resp = await axios.get("./books.json");
console.log("返回的数据是", resp.data);
renderBooks(resp.data);
} catch (error) {
console.error("请求失败", error);
}
}
如果请求失败,error 里往往会带有很关键的信息。
断点要打在两个地方
调试 Ajax,最值得打断点的地方一般有两个:
- 发请求之前。
- 请求成功或失败之后。
这样你就能分清楚:
- 到底有没有走到发请求这一步。
- 请求回来之后,程序有没有进入成功分支。
- 数据拿到了之后,是请求错了,还是渲染错了。
最常见的几类问题
| 现象 | 常见原因 | 你应该先看哪里 |
|---|---|---|
| 请求 404 | 路径写错了,文件不在那个位置 | Network 里的 URL |
| 请求没发出去 | 按钮事件没绑上,或者代码根本没执行 | Console、断点 |
| 返回了但页面没更新 | 数据拿到了,但 DOM 渲染逻辑写错了 | Console、Elements |
| 提示跨域错误 | 请求地址和页面地址不在同一个源 | Console、Network |
| json 解析失败 | json 文件格式本身有错误 | Response、Console |
你应该主动做的几种测试
不要只测“成功”这一种情况。至少可以主动试这几种:
- 把
books.json改成错误路径,看看 404 时页面是否有提示。 - 故意把网络切换到慢速,观察“正在加载...”是否会正常显示。
- 故意写错一条 json,看看控制台报什么错。
- 请求成功后,确认页面里的列表项数量和 json 数据条数一致。
开发者工具的 Network 面板里可以手动切换网络速度,这对理解“为什么异步不会卡死页面”很有帮助。
小结
这一章真正要掌握的,不只是“会发一个 axios 请求”,而是下面这四件事:
- Ajax 的意义,是不刷新整张页面,只更新需要变化的部分。
- 请求回来的数据,通常在
resp.data里。 - 异步意味着依赖请求结果的逻辑,必须写在
then回调或await之后。 - 调试 Ajax 时,先看
Network,再看Console,不要靠猜。
练习
在上面的图书列表示例中继续增加两个功能:
- 加载失败时,把错误提示显示成红色。
- 列表为空时,在页面中显示“暂无数据”。
