前言
Ajax 不是魔法,而是你掌控异步请求的第一把钥匙。
在现代 Web 开发中,页面无需刷新即可与服务器通信——这背后的核心技术就是 Ajax(Asynchronous JavaScript and XML) 。尽管名字里有 “XML”,但如今我们几乎都用 JSON 作为数据格式。本文将带你从最底层的 XMLHttpRequest 出发,一步步封装自己的 getJSON 方法,并最终理解现代 fetch API 的设计哲学。
一、Ajax 是什么?为什么需要它?
Ajax 允许网页在不重新加载整个页面的前提下,向服务器发送请求并动态更新部分内容。这种“局部刷新”极大提升了用户体验,也奠定了前后端分离架构的基础。
其核心技术依赖于浏览器提供的:
- 原生
XMLHttpRequest(XHR)对象(传统方式) - 或现代
fetchAPI(基于 Promise)
整个过程是异步的,不会阻塞主线程,用户可继续操作页面。
二、原生 Ajax:繁琐但可控
使用 XMLHttpRequest 发起一个 GET 请求的基本流程如下:
// 1. 创建 XHR 对象
const xhr = new XMLHttpRequest();
// 2. 配置请求(方法、URL、是否异步)
xhr.open('GET', '/api/data', true);
// 3. 发送请求
xhr.send();
// 4. 监听状态变化
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText); // 解析 JSON
console.log(data);
}
};
// 5. 捕获网络级错误
xhr.onerror = function () {
console.error('请求失败:发生了网络错误(如断网、CORS 被阻止等)');
};
🔍 关键知识点解析
1. xhr.onreadystatechange
该事件监听 readyState 的变化。readyState 有 5 种状态:
| 值 | 状态名 | 含义 |
|---|---|---|
| 0 | UNSENT | 尚未调用 open() |
| 1 | OPENED | 已调用 open(),连接建立 |
| 2 | HEADERS_RECEIVED | 已收到响应头 |
| 3 | LOADING | 正在接收响应体(可能部分可用) |
| 4 | DONE | 请求完成(无论成功或失败) |
✅ 只有当
readyState === 4时,才能安全读取响应内容。
2. xhr.onerror
仅在网络层面失败时触发,例如:
- 断网
- DNS 解析失败
- 跨域请求被浏览器拦截(CORS)
⚠️ 重要提醒:
- HTTP 状态码如
404、500不会触发onerror!它们属于“成功收到响应但业务失败”,需通过xhr.status判断。 onerror不提供具体错误信息(出于安全限制)。
三、手撕 Ajax:用 Promise 封装 XHR
原生 XHR 冗长且容易出错。我们能否像调用函数一样使用 Ajax?
答案是:用 Promise 封装它!
✨ 目标:实现一个 getJSON(url) 函数
期望调用方式:
getJSON('/api/user')
.then(data => console.log(data))
.catch(err => console.error(err));
这正是 Promise 的典型应用场景!
📦 初版实现(存在缺陷)
function getJSON(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
resolve(data); // 返回解析后的 JS 对象
}
};
xhr.onerror = function () {
reject(console.log("请求失败")); // ❌ 错误写法!
};
});
}
⚠️ 问题分析
- HTTP 错误未处理:若返回 404,Promise 会“卡住”(既不 resolve 也不 reject)。
- reject 参数不当:
console.log()返回undefined,导致错误信息丢失。
✅ 优化后的健壮版本
javascript
编辑
const getJSON = (url) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data); // 返回解析后的 JS 对象
} catch (e) {
reject(new Error('响应不是有效的 JSON'));
}
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
}
};
xhr.onerror = function () {
reject(new Error('网络错误:请求无法完成(可能因跨域、断网等)'));
};
});
};
💡 现在,
getJSON真正做到了:传入 URL,返回 JSON 数据,自动处理各类错误。
四、现代方案:fetch —— 浏览器内置的“手撕成果”
其实,我们刚刚封装的 getJSON,本质上就是 fetch 的简化版!
使用 fetch 发起请求
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json(); // 自动解析 JSON
})
.then(data => console.log(data))
.catch(error => console.error('请求失败:', error));
✅ fetch 的优势:
- 基于 Promise,天然支持链式调用
- API 简洁,语义清晰
- 支持现代特性(如 AbortController、流式响应等)
🔍 注意:
fetch不会因 HTTP 错误(如 404/500)自动 reject,需手动检查response.ok。
更优雅的写法:async/await
async function fetchData() {
try {
const res = await fetch('/api/data');
if (!res.ok) throw new Error('请求失败');
const data = await res.json();
console.log(data);
} catch (err) {
console.error('Error:', err);
}
}
五、Ajax 的核心价值与注意事项
✅ 优点
- 无刷新交互:提升用户体验
- 按需加载:减少带宽消耗,降低服务器压力
- 前后端解耦:前端通过 API 与后端通信,架构更清晰
⚠️ 注意事项
| 问题 | 解决方案 |
|---|---|
| 跨域(CORS) | 后端设置 Access-Control-Allow-Origin |
| SEO 不友好 | 使用 SSR(服务端渲染)或预渲染(Prerender) |
| 错误处理复杂 | 统一使用 .catch(),区分网络错误与 HTTP 错误 |
| 兼容性 | fetch 不支持 IE;老项目可用 axios 或 polyfill |
六、总结:Ajax 的演进脉络
| 阶段 | 技术 | 特点 |
|---|---|---|
| 原始时代 | XMLHttpRequest + 回调 | 繁琐、易错、回调地狱 |
| 过渡时代 | 手写 Promise 封装 XHR | 逻辑清晰、可复用 |
| 现代时代 | fetch / axios | 标准化、简洁、功能强大 |
从手撕 Ajax 到拥抱 fetch,我们不仅学会了封装,更理解了异步编程的本质:用确定的方式,处理不确定的结果。
掌握 Promise 和 Ajax 的底层原理,不仅能写出更健壮的代码,也能在面对 axios、umi-request 等高级封装库时,做到“知其然,更知其所以然”。