嘿,我是 Agnes。今天咱们不聊虚的,直接干点硬核的。
你还记得刚学前端那会儿,为了发一个 POST 请求,对着 XMLHttpRequest 写了一堆回调函数,最后发现还要手动处理 JSON 解析,整个人都崩溃了吗?😅 别担心,现在的我们早就站在巨人的肩膀上了。但是,站在巨人的肩膀上不代表你就懂了巨人是怎么长成的。
这篇文章,我要带你把 AJAX 的“三代同堂”——XMLHttpRequest、fetch、axios——从头到脚扒干净。不只是贴代码,我们要聊的是:为什么会有这些变化?在实际项目里,那些让你抓狂的报错(比如 CORS、401、超时)到底怎么破?
我会用最通俗的大白话,配合真实场景的代码示例,帮你建立一个清晰的认知地图。准备好了吗?咱们开始。
一、 那个“老祖宗”:XMLHttpRequest (XHR)
虽然我们现在很少直接手写 XHR 了,但你必须了解它。因为 fetch 和 axios 都是它的“进化版”,理解它的坑,你才能理解后来的库是怎么帮你填坑的。
1.1 基本用法回顾
XHR 是浏览器最早提供的异步通信接口。它的 API 设计得有点“反人类”,尤其是那个事件驱动的模式。
function requestWithXHR(url, method, data) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 1. 初始化请求
xhr.open(method, url, true); // true 表示异步
// 2. 设置请求头(比如告诉服务器我发的是 JSON)
xhr.setRequestHeader('Content-Type', 'application/json');
// 3. 监听状态变化
xhr.onreadystatechange = function () {
// 注意:readyState 4 表示请求完成,但还要看 status
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
// 成功,解析 JSON
resolve(JSON.parse(xhr.responseText));
} else {
// 失败,比如 404, 500
reject(new Error(`XHR Error: ${xhr.status}`));
}
}
};
// 4. 监听错误事件(注意:这和 readyState 4 + status 错误是分开的!)
xhr.onerror = function () {
reject(new Error('Network Error'));
};
// 5. 发送请求
xhr.send(data ? JSON.stringify(data) : null);
});
}
// 使用
requestWithXHR('https://api.example.com/users', 'GET')
.then(data => console.log('Got users:', data))
.catch(err => console.error('Failed:', err));
1.2 XHR 的三大痛点
- API 冗余且复杂:一堆事件(
onload,onerror,onreadystatechange…),新手极易混淆。 - 默认不发送 Cookie:同源请求下,
withCredentials需要手动设置为true。 - 类型不安全:你拿到的是
responseText(字符串),必须手动JSON.parse。如果服务器返回的是 XML,你还得解析 XML。
💡 小贴士:有些老项目里还能看到
xhr.onload和xhr.onreadystatechange混用,千万别这么干!onload只会在请求完成且成功/失败时触发,而onreadystatechange会在每一个状态变化时触发,容易重复执行。
二、 现代标准:fetch API
fetch 是 W3C 标准化的新一代 AJAX 方案,出现在 ES6 时代。它的设计哲学是:简单、基于 Promise。
2.1 基本用法
// GET 请求
fetch('https://api.example.com/users')
.then(response => {
// 注意:这里 response 不是数据,而是 Response 对象!
if (!response.ok) {
throw new Error(`HTTP Error: ${response.status}`);
}
return response.json(); // 手动解析 JSON
})
.then(data => console.log(data))
.catch(err => console.error(err));
// POST 请求
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({ name: 'Agnes', age: 25 })
})
.then(res => res.json())
.then(data => console.log('Created:', data))
.catch(err => console.error('Failed:', err));
2.2 fetch 的“坑”:为什么老手也会栽跟头?
虽然 fetch 比 XHR 简洁,但它有几个反直觉的行为,这些是面试和实际开发中的高频考点。
坑 1:失败不会 Reject,只有网络错误才会 Reject
这是最让人困惑的地方!
fetch('https://api.example.com/nonexistent')
.then(res => {
console.log(res.status); // 404
console.log(res.ok); // false
// ⚠️ 注意:这里没有抛出异常,程序会继续往下走!
return res.json();
})
.then(data => console.log(data))
.catch(err => {
// 这个 catch 不会捕获 404 或 500!
// 它只会捕获网络断开、DNS 解析失败等“网络层”错误。
console.log('Network Error:', err);
});
结论:fetch 的 Promise 只有在 网络错误(如断网、CORS 被阻止)时才会进入 .catch()。如果服务器返回 404 或 500,Promise 依然是 fulfilled 状态。所以,你必须在 .then() 里手动检查 response.ok 或 response.status。
坑 2:默认不发送 Cookie 和凭证
// 同域下,fetch 默认不携带 Cookie
fetch('/api/user/profile') // 这个请求不会带 Cookie!
// 需要手动指定
fetch('/api/user/profile', {
credentials: 'include' // 或者 'same-origin'
});
坑 3:不支持超时控制
fetch 本身没有 timeout 选项。如果你想让请求 5 秒后超时,得用 AbortController。
const controller = new AbortController();
const signal = controller.signal;
// 设置 5 秒超时
setTimeout(() => controller.abort(), 5000);
fetch('https://api.example.com/slow-endpoint', { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求被中止(超时)');
} else {
console.error('其他错误:', err);
}
});
🎯 专家点评:
fetch是底层工具,它把控制权交给了开发者。这意味着你更灵活,但也更累。你需要自己处理:JSON 解析、错误判断、超时、重试、取消请求。在实际项目中,很少有人直接裸用 fetch,除非是在封装库的时候。
三、 最受欢迎的神器:Axios
如果说 XHR 是“原始人”,fetch 是“现代人”,那 axios 就是“配备了豪华座椅和自动导航的跑车”。
Axios 是一个基于 Promise 的 HTTP 客户端,它可以运行在浏览器和 Node.js 中。它的核心优势是:易用、功能强大、生态丰富。
3.1 为什么选择 Axios?
- 自动 JSON 转换:请求时自动
JSON.stringify,响应时自动JSON.parse。 - 强大的拦截器(Interceptors):这是 axios 最厉害的功能之一!你可以在请求发出前或响应返回后,统一处理日志、Token 注入、全局错误提示等。
- 自动转换请求和响应数据:你不需要手动
res.json()。 - 默认携带 Cookie:在同源请求下,默认携带 Cookie。
- 支持请求取消:比 fetch 的
AbortController更简单易用。 - 浏览器兼容性更好:虽然现代浏览器都支持 fetch,但 axios 支持更老的浏览器(如 IE11)。
3.2 基本用法
import axios from 'axios';
// GET 请求
axios.get('/api/users')
.then(response => {
console.log(response.data); // 直接是解析好的 JSON,不需要 .json()
console.log(response.status); // 状态码
console.log(response.headers); // 响应头
})
.catch(error => {
console.error(error.response); // 如果服务器返回了错误状态码,error.response 存在
console.error(error.message); // 错误信息
});
// POST 请求
axios.post('/api/users', {
name: 'Agnes',
age: 25
}, {
headers: {
'Authorization': 'Bearer token123'
}
})
.then(res => console.log(res.data))
.catch(err => console.error(err));
3.3 核心功能:拦截器(Interceptors)
这是 axios 区别于 fetch 的最大亮点。想象一下,你的项目有 100 个页面,每个页面都要带 Token。如果每个请求都手动加 Header,维护起来会疯掉。
用拦截器,只写一次,全局生效。
请求拦截器:自动注入 Token
axios.interceptors.request.use(
config => {
// 从 localStorage 获取 token
const token = localStorage.getItem('access_token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 可以打印请求日志,方便调试
console.log('🚀 发出请求:', config.method.toUpperCase(), config.url);
return config;
},
error => {
// 处理请求错误
return Promise.reject(error);
}
);
响应拦截器:统一处理错误和 Token 刷新
axios.interceptors.response.use(
response => {
// 2xx 状态码会进入这里
return response.data; // 直接返回 data,这样业务代码里就不用写 .data 了
},
error => {
const { response } = error;
if (response) {
switch (response.status) {
case 400:
console.error('请求参数错误');
break;
case 401:
// Token 过期了!
console.warn('Token 过期,即将跳转登录页...');
// 清除本地存储
localStorage.removeItem('access_token');
// 跳转到登录页
window.location.href = '/login';
break;
case 403:
console.error('没有权限访问');
break;
case 404:
console.error('资源不存在');
break;
case 500:
console.error('服务器内部错误');
break;
default:
console.error(`其他错误: ${response.status}`);
}
} else if (error.code === 'ECONNABORTED') {
console.error('请求超时');
} else {
console.error('网络错误:', error.message);
}
return Promise.reject(error);
}
);
🎯 专家点评:拦截器是 axios 的灵魂。在真实项目中,99% 的 axios 封装都会用到拦截器来处理 Token 和全局错误。
3.4 创建自定义实例(Instance)
一个大型项目可能有多个 API 服务(比如主站 API 和支付 API),它们的 base URL 和超时时间不同。这时,你可以创建多个 axios 实例。
// 主站 API 实例
const mainApi = axios.create({
baseURL: 'https://api.example.com/v1',
timeout: 10000,
headers: {
'X-Custom-Header': 'MainApp'
}
});
// 支付 API 实例(超时更短,安全要求更高)
const paymentApi = axios.create({
baseURL: 'https://payment.example.com/api',
timeout: 5000,
headers: {
'X-Payment-Service': 'True'
}
});
// 为支付 API 单独添加拦截器
paymentApi.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 402) {
// 支付相关错误,弹窗提示用户充值
alert('支付失败,请检查您的余额');
}
return Promise.reject(error);
}
);
// 使用
mainApi.get('/users').then(data => console.log(data));
paymentApi.post('/charge', { amount: 100 }).then(data => console.log(data));
四、 三者对比:一张表说清楚
为了让你更直观地理解,我整理了一个对比表:
| 特性 | XMLHttpRequest | fetch | Axios |
|---|---|---|---|
| API 风格 | 回调函数 | Promise | Promise |
| JSON 自动转换 | ❌ 需手动 | ❌ 需手动 | ✅ 自动 |
| 默认发送 Cookie | ✅ 同源自动 | ❌ 需配置 credentials |
✅ 同源自动 |
| 超时控制 | ❌ 不支持 | ⚠️ 需 AbortController |
✅ 原生支持 timeout |
| 请求取消 | ⚠️ 需 xhr.abort() |
✅ AbortController |
✅ CancelToken 或 AbortController |
| 拦截器 | ❌ 不支持 | ❌ 不支持 | ✅ 强大的请求/响应拦截 |
| 错误处理 | ⚠️ 复杂 | ⚠️ 需手动判断 status | ✅ 统一处理 |
| 浏览器支持 | ✅ 所有浏览器 | ⚠️ 不支持 IE | ✅ 所有浏览器 |
| 文件大小 | - | 内置,无额外体积 | ~13KB (gzip) |
| 适用场景 | 老旧项目维护 | 简单项目、轻量级需求 | 企业级项目、复杂业务 |
🎯 专家点评:如果项目非常轻量,且不需要兼容 IE,
fetch完全够用。但如果是一个中大型项目,需要统一管理 Token、处理全局错误、支持请求取消,axios 是不二之选。
五、 真实项目中的常见报错及解决方案
作为前端,你肯定会遇到各种各样的报错。下面我列举几个最典型、最让人头疼的场景,并给出解决方案。
5.1 CORS 跨域错误
现象:控制台报错 Access to XMLHttpRequest at '...' from origin '...' has been blocked by CORS policy
原因:浏览器的同源策略限制。你的前端域名(比如 localhost:3000)请求了后端域名(比如 api.example.com),浏览器认为这是跨域请求,需要后端配合返回特定的响应头。
解决方案:
后端配置(最根本的解决方案): 后端需要在响应头中添加:
Access-Control-Allow-Origin: http://localhost:3000 # 或者 * 表示允许所有 Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization前端开发环境代理(开发时常用): 在
vue.config.js或webpack.config.js中配置代理,把请求“转发”到后端,避免浏览器检测到跨域。Vue CLI 配置示例:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://api.example.com', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' } // 把 /api/users 变成 http://api.example.com/users } } } };这样,你在前端请求
/api/users时,浏览器认为这是同源请求,而代理服务器会把请求转发到http://api.example.com/users。fetch 的 credentials 问题: 如果你使用了
credentials: 'include',但后端没有配置Access-Control-Allow-Origin: *(必须指定具体域名),请求依然会失败。这是常见的坑!
5.2 401 Unauthorized:Token 过期或无效
现象:请求返回 401 状态码,页面突然“失忆”,用户需要重新登录。
解决方案:
在 axios 响应拦截器中处理(前面已经讲过):
if (error.response?.status === 401) { // 清除 token localStorage.removeItem('token'); // 跳转到登录页 router.push('/login'); }Token 自动刷新(Refresh Token 机制): 更优雅的做法是:当 401 发生时,自动用 Refresh Token 换取新的 Access Token,然后重试原请求。
”`javascript // 伪代码示例 let isRefreshing = false; let pendingRequests = [];
axios.interceptors.response.use( response => response, async error => {
const
