嘿,朋友,我是 Agnes。今天咱们不聊那些让人头大的枯燥文档,来聊聊前端开发里最经典、也是用得最多的老朋友——AJAX。
很多人一听“AJAX”就想到 fetch,仿佛它是新欢。但说实话,如果你不懂 XMLHttpRequest(简称 XHR)这个“前任”,你就很难真正理解现代网络请求的底层逻辑。毕竟,fetch 本质上就是个更优雅的外壳,而 XHR 才是那个在幕后默默干活几十年的老黄牛。
准备好了吗?咱们先把那个看似古老、实则深不可测的 XMLHttpRequest 扒开看看,然后再聊聊 GET 和 POST 的那些“恩怨情仇”。
一、 为什么还要讲 XMLHttpRequest?
你可能会问:“Agnes,现在谁还用 XHR 啊?直接用 fetch 或者 axios 不香吗?”
问得好。但我敢打赌,如果你去面试,或者去维护一些老项目(是的,大厂里的存量代码可能比你想象的还多),你一定会遇到它。而且,理解了 XHR,你才能真正理解 HTTP 请求的状态机是怎么工作的。
XHR 是浏览器的一个内置对象,早在 IE5 时代就出现了。它允许 JavaScript 在页面不刷新的情况下,与服务器交换数据。
咱们先看一个最原始的 XHR 请求是什么样的,别被它的代码量吓跑,我会带你逐行拆解。
// 1. 创建 XHR 对象
const xhr = new XMLHttpRequest();
// 2. 初始化请求:指定方法(GET/POST)、URL、是否异步
xhr.open('GET', 'https://api.example.com/users', true);
// 3. 设置请求头(可选,但很重要)
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer my-token');
// 4. 监听状态变化(这是 XHR 最核心的部分)
xhr.onreadystatechange = function () {
// readyState 为 4 表示请求已完成,响应已就绪
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
// 成功处理
console.log('数据回来了:', xhr.responseText);
const data = JSON.parse(xhr.responseText);
renderUI(data);
} else if (xhr.status >= 400) {
// 客户端错误
console.error('请求失败,状态码:', xhr.status);
} else if (xhr.status === 0) {
// 状态为0通常意味着网络错误或请求被取消
console.error('网络连接失败或请求被中断');
}
}
};
// 5. 发送请求
xhr.send();
看着是不是有点复杂?别急,咱们把这五个步骤掰碎了讲。
步骤拆解:读懂 XHR 的“方言”
第一步:new XMLHttpRequest()
这就像是你去邮局寄信,先得有个信封。这个对象就是你和服务器沟通的通道。
第二步:xhr.open(method, url, async)
这是“填信封”的过程。
method: 你想用什么方式寄信?GET(只读查询)还是 POST(提交数据)?url: 收件人地址。async: 是否异步。默认是true。如果设为false,浏览器会“卡死”直到请求完成,这在现代前端是绝对的禁忌,会让用户体验极差。所以,永远保持true。
第三步:xhr.setRequestHeader(...)
这是“贴邮票”或“写备注”。服务器有时候需要知道你是谁、你要传什么格式的数据。比如 Content-Type: application/json 就是在告诉服务器:“嘿,我发过去的数据是 JSON 格式的,别搞错了。”
第四步:onreadystatechange
这是 XHR 最让人困惑的地方,也是它最强大的地方。
XHR 有一个 readyState 属性,它有 5 个状态:
0: 未初始化(还没调用 open)1: 已打开(调用了 open,但还没 send)2: 已发送(调用了 send,正在处理中)3: 接收中(正在接收响应数据)4: 完成(所有数据都接收完毕)
大部分时候,我们只关心状态 4。但如果你要做“进度条”或者“实时流数据”,状态 3 就非常有用了。
第五步:xhr.send(body)
最后一步,“扔进邮筒”。
- 如果是 GET 请求,
send()里通常传null或什么都不传,因为数据放在 URL 里了。 - 如果是 POST 请求,你需要把数据作为参数传进去。
二、 GET 与 POST:那些年被误解的区别
讲完 XHR 怎么用,咱们得聊聊核心:GET 和 POST 到底有啥区别?
很多教程会列出一堆术语,什么“幂等性”、“安全性”、“缓冲机制”。说实话,这些词离开发者太远了。咱们用大白话,结合生活场景来理解。
场景类比:去餐厅吃饭
想象一下,你和朋友去餐厅。
GET 请求:就像是你在门口喊一声:“老板,给我来一份宫保鸡丁!”
- 你的要求(参数)都写在嘴上了(URL 里)。
- 老板听清楚后,去厨房做菜,然后端上来。
- 你不需要填任何单子,也不需要把食物“送”进厨房。
- 特点:数据暴露在 URL 中,有长度限制(因为 URL 长度有限),适合查询、获取数据。
POST 请求:就像是你在点菜本上写下“宫保鸡丁”,然后郑重其事地递给服务员。
- 你的要求(参数)写在身体里(请求体 Body 中),老板看不到。
- 服务员拿着单子去厨房,厨房根据单子做菜。
- 特点:数据不在 URL 中,更安全,没有严格的长度限制,适合提交敏感数据(如密码)、大文件上传、修改数据。
技术层面的硬核区别
虽然上面是类比,但技术细节还是得讲清楚,因为面试和工作中经常会用到。
| 特性 | GET | POST |
|---|---|---|
| 语义 | 从服务器获取数据 | 向服务器提交数据 |
| 参数位置 | URL 查询字符串 (?key=value) |
请求体 (Body) |
| 可见性 | 参数暴露在地址栏,易被缓存、收藏 | 参数不可见,更私密 |
| 长度限制 | 受限于浏览器和服务器对 URL 长度的限制(通常 2KB-8KB) | 理论上无限制,受服务器配置影响 |
| 幂等性 | 是。多次请求结果相同,不会改变服务器状态 | 否。多次提交可能产生多个结果(如多次下单) |
| 缓存 | 可被浏览器缓存 | 默认不缓存 |
| 安全性 | 较低(数据明文) | 相对较高(数据在 Body 中) |
代码对比:用 XHR 实现 GET 和 POST
咱们直接写代码,看看在 XHR 中,GET 和 POST 有啥不同。
GET 请求示例:
function getData() {
const xhr = new XMLHttpRequest();
// GET 请求,数据直接拼在 URL 里
const url = 'https://api.example.com/users?name=张三&age=25';
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log('GET 成功:', JSON.parse(xhr.responseText));
}
};
xhr.send();
}
POST 请求示例:
function postData() {
const xhr = new XMLHttpRequest();
const url = 'https://api.example.com/users';
xhr.open('POST', url, true);
// 必须设置 Content-Type,告诉服务器我们传的是 JSON
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
// 定义要发送的数据
const data = JSON.stringify({
name: '李四',
age: 30,
email: 'lisi@example.com'
});
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log('POST 成功:', JSON.parse(xhr.responseText));
}
};
// POST 请求,数据作为 send 的参数
xhr.send(data);
}
注意看,POST 请求中,send() 里传的是 data,而 GET 请求里 send() 是空的。这就是最直观的区别。
三、 进阶:XHR 的痛点与现代解决方案
聊了这么多 XHR,你可能会发现一个问题:回调地狱。
当你需要发送多个请求,并且后一个请求依赖前一个请求的结果时,代码会变得像俄罗斯套娃一样嵌套:
xhr1.send();
// ...
// 在 xhr1 的回调里发 xhr2
xhr2.send();
// ...
// 在 xhr2 的回调里发 xhr3
xhr3.send();
// ...
这简直是一场噩梦。随着项目变大,这种代码会变成“面条代码”,难以维护。
解决方案一:Promise 封装
聪明的开发者发明了 axios,它内部其实就是把 XHR 封装了一下,让它支持 Promise。但如果你不想引入第三方库,可以自己封装一个。
function ajaxRequest(method, url, data = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
if (method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
}
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`请求失败: ${xhr.status}`));
}
};
xhr.send(data ? JSON.stringify(data) : null);
});
}
// 使用方式,清爽多了!
ajaxRequest('POST', 'https://api.example.com/users', { name: '王五', age: 28 })
.then(res => console.log('添加用户成功:', res))
.catch(err => console.error('出错了:', err));
解决方案二:现代 fetch API
现代浏览器原生提供了 fetch,它基于 Promise,语法更简洁,且不需要手动解析 JSON(虽然还是需要 .json() 方法)。
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: '赵六', age: 35 })
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应不正常');
}
return response.json();
})
.then(data => console.log('成功:', data))
.catch(error => console.error('失败:', error));
可以看到,fetch 比原始的 XHR 简洁了很多,但它底层依然还是用的 XHR 或者类似的机制(在某些旧浏览器中)。
四、 常见陷阱与最佳实践
作为过来人,我得提醒你几个在写 AJAX 请求时容易踩的坑。
1. 跨域问题(CORS)
你肯定遇到过这个报错:Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy。
跨域是指你的前端页面(比如 localhost:3000)请求了另一个域名(比如 api.example.com)的接口。出于安全考虑,浏览器默认会拦截这种请求。
解决办法:
- 后端配合:后端需要在响应头中添加
Access-Control-Allow-Origin: *或者指定你的域名。 - 开发环境代理:在本地开发时,可以通过 Webpack、Vite 或 Nginx 配置代理,把请求转发到后端,绕过浏览器的跨域限制。
2. 请求超时处理
默认情况下,XHR 没有超时时间,可能会一直挂着。你得手动设置。
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/slow-data', true);
// 设置超时时间为 5 秒
xhr.timeout = 5000;
// 监听超时事件
xhr.ontimeout = function () {
console.error('请求超时,请检查网络或稍后重试');
};
xhr.send();
3. 请求取消
有时候,用户快速点击按钮,或者页面跳转了,但我们之前的请求还没回来。这时候应该取消请求,避免不必要的资源浪费和状态混乱。
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/search?q=' + query, true);
// 在合适的时候调用 abort()
function cancelRequest() {
if (xhr.readyState !== 0 && xhr.readyState !== 4) {
xhr.abort();
console.log('请求已取消');
}
}
// 比如用户重新输入时,取消上一个请求
inputElement.addEventListener('input', (e) => {
cancelRequest();
// 重新发起请求...
});
4. 同步请求的禁忌
再次强调,千万不要在 open 中设置 async: false。这会导致浏览器 UI 线程阻塞,用户会感觉页面“卡死”了,体验极差。现代浏览器甚至正在逐步废弃同步 XHR。
五、 总结:从 XHR 到未来
好了,朋友们。咱们今天聊了 XHR 的创建、状态管理、GET 与 POST 的区别,还看了怎么用 Promise 封装它,以及如何应对跨域和超时。
我知道,你大概率不会在生产环境中直接写原生的 XMLHttpRequest。你会用 fetch,或者用 axios、ky 这些库。但是,理解 XHR 的内部机制,能帮你更好地理解这些工具在背后做了什么。
- GET 是查询,像喊话,数据在 URL 里,安全级别低,适合拿数据。
- POST 是提交,像填表,数据在 Body 里,安全级别高,适合改数据。
- XHR 是根基,它的
readyState和status概念贯穿了整个前端网络请求领域。
最后,我想说的是,技术一直在迭代,从 XHR 到 jQuery.ajax,再到 fetch,再到现在的 React Query、SWR。工具在变,但网络请求的本质没变:建立连接、发送数据、处理响应。
希望这篇文章能帮你把 AJAX 这个知识点彻底吃透。如果你在实践中遇到什么奇怪的问题,欢迎随时来找我聊聊。毕竟,每一个 bug 背后,都是一次成长的机会。
加油,未来的前端大牛!
