写前端代码这几年,我见过太多人在“异步请求”这个环节栽跟头。有时候是因为一个奇怪的跨域报错卡了半小时,有时候是因为 $.ajax 里的 async: false 导致了严重的性能问题,还有时候是因为搞不清 fetch 返回的 Promise 状态。
其实吧,这三个东西本质干的是同一件事:让浏览器在后台偷偷去服务器拿数据,别让页面卡死。 但它们的写法、陷阱和排查思路完全不同。今天咱就把这三件“老朋友”拉出来溜溜,顺便把那些让人头疼的报错一一消灭。
XMLHttpRequest:那个“老古董”依然很能打
先说 XMLHttpRequest(简称 xhr)。这东西诞生于1999年,比 JavaScript 本身都老不了多少。虽然现在大家都追捧 fetch,但在很多老项目里,甚至一些对兼容性要求极高的场景下,xhr 依然是主力。
基本用法长这样
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data', true); // 第三个参数 true 表示异步
xhr.onreadystatechange = function () {
// readyState 有 5 个状态
if (xhr.readyState === 4) { // 4 表示请求完成
if (xhr.status === 200) {
console.log('成功啦:', JSON.parse(xhr.responseText));
} else {
console.error('请求失败,状态码:', xhr.status);
}
}
};
xhr.send();
你看,这代码是不是有点啰嗦?readyState、status、responseText… 一堆东西要记。但好处是,它能给你极细粒度的控制。比如,你想监听上传进度:
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
var percentComplete = (event.loaded / event.total) * 100;
console.log('上传进度:' + percentComplete + '%');
}
};
fetch 做不到这个,这是 xhr 的一大优势。
常见报错:别被 onerror 骗了
很多新手用 xhr 时,会写这样的代码:
xhr.onerror = function() {
console.log('出错了');
};
然后发现,哪怕服务器返回 404,这个 onerror 也不会触发。为什么?因为 xhr 的 onerror 只在网络请求完全失败时才触发,比如断网、DNS 解析失败、跨域问题被浏览器拦截等。如果服务器正常响应了(哪怕是 404、500),它都算“成功”,只是状态码不对而已。
所以,正确的姿势是:
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('成功');
} else {
console.error('HTTP 错误:', xhr.status);
}
};
xhr.onerror = function() {
console.error('网络错误,可能断网或跨域被拦截');
};
记住:状态码 2xx 才是真的成功,别只依赖 onload 就以为万事大吉。
Fetch:现代浏览器的新宠
2015 年左右,fetch 出现了。它的理念很简单:用 Promise 来简化异步编程。语法优雅,链式调用,看起来比 xhr 高级多了。
基本用法
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error('HTTP 错误:' + response.status);
}
return response.json();
})
.then(data => {
console.log('成功啦:', data);
})
.catch(error => {
console.error('出错了:', error);
});
看起来是不是清爽很多?但这里有个巨大的坑,几乎所有开发者都踩过。
最大的坑:fetch 不会 rejection 掉 HTTP 错误
看上面那段代码,我特意加了 if (!response.ok) 的判断。为什么?因为 fetch 默认情况下,即使服务器返回 404 或 500,Promise 依然是 resolved 状态,它不会自动 throw error。只有网络完全失败(比如断网)时,才会进入 catch。
这就导致很多人写这样的代码:
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data));
结果服务器返回 400,res.json() 成功执行了,只是拿到的数据可能是个错误提示,然后你的页面就崩了,而你还不知道哪儿出了问题。
所以,必须手动检查 response.ok 或 response.status。
发送 POST 请求
fetch('/api/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: '张三', age: 25 })
})
.then(res => {
if (!res.ok) throw new Error('提交失败');
return res.json();
})
.then(data => console.log('提交成功', data))
.catch(err => console.error(err));
注意 headers 要自己设置 Content-Type,不像 jQuery 那样自动序列化。这是 fetch 的一个小麻烦。
取消请求
fetch 用 AbortController 来取消:
const controller = new AbortController();
const signal = controller.signal;
fetch('/api/data', { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('请求被取消了');
} else {
console.error('其他错误', err);
}
});
// 3秒后取消
setTimeout(() => controller.abort(), 3000);
xhr 也能取消(用 xhr.abort()),但 fetch 的 AbortController 更符合现代 API 设计。
jQuery $.ajax:曾经的王者
在 fetch 出现之前,$.ajax 几乎是前端开发的标配。它的优点是把复杂的 xhr 封装得非常友好,自动处理很多细节,写起来省心。
基本用法
$.ajax({
url: 'https://api.example.com/data',
type: 'GET',
dataType: 'json',
success: function(data) {
console.log('成功:', data);
},
error: function(xhr, status, error) {
console.error('失败:', status, error);
}
});
自动处理 JSON
jQuery 最香的一点是 dataType: 'json',它会自动帮你把响应解析成 JSON 对象,不用你手动 JSON.parse()。而且,如果服务器返回的是非 JSON 格式,jQuery 会报错,这反而有助于早期发现问题。
发送请求
$.ajax({
url: '/api/user',
type: 'POST',
data: { name: '张三', age: 25 },
contentType: 'application/x-www-form-urlencoded',
success: function(data) {
console.log('成功', data);
},
error: function(xhr, status, err) {
console.error('失败', status, err);
}
});
注意,jQuery 默认用 application/x-www-form-urlencoded 格式发送数据,这和 fetch 的默认行为不同。如果你想用 JSON,需要手动设置:
$.ajax({
url: '/api/user',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({ name: '张三', age: 25 }),
success: function(data) { ... },
error: function(xhr, status, err) { ... }
});
全局错误处理
jQuery 有个很实用的功能:$.ajaxError()。它可以监听整个页面的所有 ajax 错误:
$(document).ajaxError(function(event, xhr, settings, thrownError) {
console.log('某个请求出错了', settings.url, thrownError);
});
这在大型项目里非常有用,可以统一处理错误提示,不需要每个请求都写 error 回调。
常见报错:statusText 和 thrownError
很多人用 error 回调时,会这样写:
error: function(xhr, status, error) {
console.log(status); // 可能是 "error" 或 "timeout"
console.log(error); // 可能是 "" 或异常消息
}
注意,status 是 jQuery 自定义的状态字符串(如 "error"、"timeout"、"abort"),而 xhr.status 才是真正的 HTTP 状态码(如 404、500)。一定要分清这两个。
另外,thrownError 这个参数在很多教程里被忽略了。它其实是浏览器抛出的异常信息,在跨域问题时特别有用:
error: function(xhr, status, thrownError) {
if (xhr.status === 0) {
console.log('可能是跨域问题或网络断开');
console.log(thrownError); // 这里可能有具体信息
}
}
三者对比:我该用哪个?
好了,代码看完了,咱来聊聊怎么选。
| 特性 | XMLHttpRequest | Fetch | jQuery $.ajax |
|---|---|---|---|
| 语法简洁度 | 差(回调地狱) | 好(Promise 链式) | 好(配置对象) |
| 错误处理 | 手动判断 status | 需手动检查 ok | 自动处理 |
| 取消请求 | xhr.abort() |
AbortController |
jqXHR.abort() |
| 上传进度 | 支持 | 不支持 | 支持(通过 xhr) |
| 浏览器兼容 | 所有浏览器 | 现代浏览器(IE 不支持) | 所有浏览器 |
| 自动 JSON 解析 | 无 | 无 | 有(dataType) |
| 代码体积 | 无依赖 | 无依赖 | 需引入 jQuery |
| 学习曲线 | 陡峭 | 平缓 | 平缓 |
我的建议
如果你在维护老项目,或者需要支持 IE:继续用 jQuery $.ajax 或原生命名封装的 xhr。没必要强行改成 fetch,增加维护成本。
如果你在做新项目,且不需要支持 IE:优先用 fetch。它是浏览器原生支持的,零依赖,语法现代,和 Promise、async/await 搭配使用非常优雅。
如果你想兼顾兼容性和现代语法:可以用 axios 库。它底层基于 fetch(或 xhr),但提供了更统一的 API,自动 JSON 解析,拦截器等功能,很多现代项目都在用。
实际案例:用 async/await 重写 fetch
现代 JavaScript 推荐使用 async/await,它让异步代码看起来像同步代码,可读性大幅提升:
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('获取用户数据失败:', error);
throw error; // 重新抛出,让调用者处理
}
}
// 使用
getUserData(123)
.then(user => console.log('用户信息:', user))
.catch(err => console.error('错误:', err));
或者直接用顶级 await(在模块中):
// 在 ES module 中
const user = await getUserData(123);
console.log(user);
常见报错排查清单
最后,我把大家最常遇到的报错和排查思路整理一下,收藏起来备用:
1. CORS 跨域错误
现象:控制台报 Access-Control-Allow-Origin 错误,请求被浏览器拦截。
排查:
- 确认后端是否设置了正确的 CORS 头:
Access-Control-Allow-Origin: *或具体域名 - 检查请求方法是否是简单请求(GET/POST/HEAD,且 Content-Type 是 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)
- 如果不是简单请求,浏览器会先发 OPTIONS 预检请求,确认后端支持
解决:后端配置 CORS,或使用代理服务器(开发环境)。
2. 404 Not Found
现象:请求返回 404,数据拿不到。
排查:
- 检查 URL 是否正确,注意大小写和路径分隔符
- 确认服务器端路由是否配置正确
- 如果是相对路径,检查基准路径是否正确
解决:修正 URL 或后端路由。
3. 401 Unauthorized / 403 Forbidden
现象:需要登录才能访问,但没传 token 或 token 无效。
排查:
- 检查请求头是否带了
Authorization字段 - 确认 token 是否过期
- 检查 token 格式是否正确(如
Bearer <token>)
解决:补充认证信息,或刷新 token。
4. 500 Internal Server Error
现象:服务器内部出错。
排查:
- 查看服务器日志,定位具体错误
- 检查请求参数是否正确,有没有缺少必填字段
- 确认服务器环境是否正常
解决:修复后端代码或调整请求参数。
5. TypeError: Failed to fetch
现象:网络完全失败,Promise 被 reject。
排查:
- 检查网络连接是否正常
- 确认 URL 格式是否正确
- 检查是否被浏览器扩展拦截(如广告拦截器)
- 如果是 HTTP 链接,确认是否被混合内容策略阻止(HTTPS 页面不能请求 HTTP 资源)
解决:修复网络问题,或使用 HTTPS。
6. jQuery 中 statusText 为空
现象:error 回调中 statusText 是空字符串。
排查:
- 这通常发生在跨域错误或网络断开时
- jQuery 的
statusText在某些情况下会被浏览器忽略
解决:改用 xhr.status 和 xhr.responseText 获取更多信息。
写在最后
异步请求这块技术,其实没有绝对的好坏,只有适不适合。xhr 虽然啰嗦,但在需要精细控制的场景下依然不可替代;fetch 简洁现代,是未来的方向;jQuery ajax 虽然老了点,但在遗留项目里依然稳定可靠。
作为开发者,我的建议是:熟悉这三种写法,知道它们各自的优缺点,然后根据项目需求灵活选择。别盲目追新,也别固步自封。
如果你在排查问题时卡住了,不妨静下心来,对照上面的清单一步步检查。大多数时候,问题就出在 URL 写错、跨域没配好、或者忘了检查 response.ok 这种小细节上。
希望这篇指南能帮到你。如果还有疑问,欢迎随时交流——毕竟,代码是写给人看的,顺便让机器执行,对吧?
