
本文将从实际场景出发,先拆解XHR的传统实现(如状态监听、回调处理)与Fetch的现代语法(Promise链、async/await结合),再深度对比两者在错误处理、credentials传递、兼容性等关键维度的差异,帮你快速理清适用场景,避开开发踩坑,轻松掌握前端异步请求的核心技巧。
你有没有过这种情况?刚学前端时,写个异步请求要绕好几个弯——用XHR吧,得写一堆onreadystatechange
监听,回调嵌套得像“回调地狱”;换Fetch吧,以为能轻松点,结果接口返回500错误,catch
却纹丝不动,急得你对着控制台挠头?其实这俩工具都是前端异步请求的“老熟人”,但用法和坑点差得远呢,今天我就用自己踩过的坑、帮同事调过的bug,跟你唠唠它们的实战用法和区别。
XHR:传统异步的“老伙计”,该怎么用才不踩坑?
XHR(XMLHttpRequest)算是前端异步请求的“开国元勋”了,从IE6时代就有,直到现在还有不少项目在用——尤其是要兼容老浏览器的时候。我当年刚学前端时,第一次用XHR做登录功能,写了二十多行代码,结果拿到的响应是字符串,得自己JSON.parse
,后来才知道要设置responseType: 'json'
,省了不少事。今天就跟你掰扯掰扯XHR的实战步骤,还有那些容易踩的“小陷阱”。
XHR的核心流程就四步:创建对象→配置请求→发送请求→监听结果。比如你要发个GET请求拿用户信息,代码大概长这样:
// 创建XHR对象
const xhr = new XMLHttpRequest();
//
配置请求:method(请求方式)、url(接口地址)、async(是否异步,默认true)
xhr.open('GET', '/api/user?id=1', true);
//
(可选)设置请求头,比如JSON格式的POST请求需要加这个
xhr.setRequestHeader('Content-Type', 'application/json');
//
(可选)设置响应类型,直接拿到JSON对象,不用自己转
xhr.responseType = 'json';
//
监听状态变化:readyState从0到4,4表示请求完成
xhr.onreadystatechange = function() {
// 先判断请求完成(readyState===4),再判断HTTP状态码(200-299是成功)
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
// 拿到响应数据,因为设置了responseType,直接是对象
console.log('请求成功:', xhr.response);
} else {
console.log('请求失败,状态码:', xhr.status);
}
}
};
//
发送请求(GET请求不用传body,POST的话传JSON字符串)
xhr.send();
是不是看着有点“繁琐”?但这些步骤里藏着很多容易踩的坑——比如去年帮一个刚入行的同事调代码,他的XHR请求一直没反应,我看了半天才发现:他漏写了send()
方法!就像你买了快递单填完信息,却没交给快递员,能收到货才怪。
再比如错误处理,XHR的onerror
事件只处理“网络错误”(比如断网、跨域没配置),如果是HTTP状态码错误(比如404、500),onerror
不会触发——得靠xhr.status
判断。我之前做一个商品列表功能,接口返回500错误,我盯着onerror
看了半天,结果发现得在onreadystatechange
里判断状态码,才把问题解决。
还有一个容易忘的点:POST请求的body格式。如果你要传JSON数据,得用JSON.stringify()
转成字符串,还要设置Content-Type: application/json
——我当年第一次做注册功能,直接把对象传给send()
,结果后端收到的是[object Object]
,查了文档才知道要转格式。
Fetch:现代异步的“新选择”,好用但得避开这些“隐形坑”
Fetch是ES6之后出的“现代异步方案”,用Promise
封装,语法比XHR简洁多了,现在很多框架(比如React、Vue)的异步请求都默认用它。但我得跟你说:Fetch不是“完美替代”,它的坑比你想象中多。
先看Fetch的基本用法——比如要拿用户列表,代码就这么几行:
async function fetchUserList() {
try {
//
发送请求,返回Promise(resolve的是Response对象)
const response = await fetch('/api/users', {
method: 'GET', // 请求方式,默认GET
headers: {
'Content-Type': 'application/json'
},
credentials: 'include' // 携带Cookie,默认不携带!
});
//
检查响应状态:response.ok是true表示200-299
if (!response.ok) {
throw new Error(请求失败,状态码:${response.status}
);
}
//
解析响应数据:json()返回Promise,拿到JSON对象
const data = await response.json();
console.log('用户列表:', data);
} catch (err) {
console.log('请求出错:', err.message);
}
}
是不是比XHR简洁多了?但这里藏着两个“致命坑”,我踩过好几次:
坑1:HTTP错误不会触发catch
Fetch的Promise
只有在“网络错误”(比如断网)时才会reject
,如果是HTTP状态码错误(比如404、500),它会resolve
!我上次做一个订单详情页,接口返回500错误,catch
却没触发,我以为是代码写错了,查了半小时才发现:得手动检查response.ok
,否则错误不会进catch
。就像你点外卖,商家给你送了份“变质的饭”(500错误),但外卖平台默认“已送达”,你得自己打开盒子看一眼才知道有问题。
坑2:默认不携带Cookie
Fetch默认不会带Cookie——如果你的接口需要用户登录状态(比如需要sessionId
),得手动加credentials: 'include'
参数。我之前做一个后台管理系统,用Fetch请求用户信息,结果一直返回“未登录”,后来才想起这个配置——就像你去餐厅吃饭,忘了带会员卡,服务员肯定不认识你啊。
坑3:不支持进度监听(默认)
XHR可以用onprogress
监听请求进度(比如文件上传的进度条),但Fetch默认不支持——得用ReadableStream
。比如你要做图片上传进度条,可以这么写:
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
const formData = new FormData();
formData.append('file', file);
// 用Fetch的body是ReadableStream,监听进度
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
// 监听上传进度
signal: AbortController().signal, // 可选:取消请求
onUploadProgress: (progressEvent) => {
const percent = (progressEvent.loaded / progressEvent.total) 100;
console.log(上传进度:${percent}%
);
}
});
});
不过这个功能是Fetch的“高级用法”,一般小项目用不到,但如果你做文件上传,可得记着这个点。
除了坑,Fetch也有不少优点:比如支持Stream
(处理大文件更高效)、语法更简洁(结合async/await
几乎没有嵌套)、API更现代化(比如Headers
对象管理请求头)。我现在做的Vue项目,所有异步请求都用Fetch,代码比用XHR时少了三分之一,可读性高多了。
最后:XHR和Fetch该怎么选?一张表帮你理清楚
说了这么多,你肯定想问:“我到底该用XHR还是Fetch?”我整理了一张对比表,从实战场景、坑点、兼容性三个维度帮你选:
对比项 | XHR | Fetch |
---|---|---|
兼容性 | 支持IE6+,老浏览器首选 | 支持Chrome 42+、Firefox 39+,IE不支持 |
错误处理 | onerror (网络错误)+ status (HTTP错误) |
需手动检查response.ok ,否则不触发catch |
Cookie携带 | 默认携带 | 需设置credentials: 'include' |
进度监听 | onprogress 直接用,简单 |
需用ReadableStream ,略复杂 |
语法复杂度 | 代码多,回调嵌套多 | Promise +async/await ,简洁易读 |
简单 (不是 哦):如果你的项目要兼容IE11或更老的浏览器,直接选XHR;如果是现代浏览器(比如Chrome、Edge),选Fetch更舒服——但得记着那些坑。比如我最近做的移动端H5项目,用户都是用微信或手机浏览器,直接用Fetch,代码写得快,也没兼容问题;但上个月帮一个国企做后台系统,他们要求支持IE11,我就老老实实用XHR,再加个polyfill
处理Promise
。
你最近用XHR或Fetch时踩过什么坑?比如有没有遇到过Fetch不触发catch
的情况?或者XHR忘了设置responseType
?欢迎留言告诉我,咱们一起避坑——毕竟前端的坑,踩一次就够了!
你要是做项目碰到得兼容IE11的情况,问能不能用Fetch?说实话,直接用肯定不行——IE11根本就不认识Fetch这个API,你写了Fetch的代码,打开IE11一试,控制台直接蹦“Fetch is undefined”的错误,白忙活一场。那有没有办法曲线救国?有的,加个polyfill比如whatwg-fetch,它能模拟Fetch的基本用法,比如发个GET请求拿用户列表,或者POST提交个表单数据,这些简单场景没问题。但你得记着,polyfill只是“模拟”,不是真的Fetch——像进度监听这种高级功能它就搞不定,比如你要做个图片上传的进度条,用polyfill的Fetch肯定拿不到进度数据,到时候又得改代码换方法,反而更费时间。
其实我 你直接用XHR得了,毕竟XHR是IE11原生就支持的,不用额外加任何依赖,稳得很。你想啊,兼容IE11的项目本来就讲究“稳定大于一切”,XHR虽然写起来繁琐点——比如要创建对象、open配置、onreadystatechange监听状态,还要自己判断status码是不是200-299——但胜在“老而可靠”,不会出那种“Chrome里跑通了,IE11里直接崩”的幺蛾子。我之前帮一个国企做后台系统,他们要求必须支持IE11,我一开始图新鲜用了Fetch加polyfill,结果做文件上传功能时,进度条死活不显示,查了半天文档才发现,polyfill根本不支持Fetch的ReadableStream进度监听,最后没办法,还是换回XHR,用onprogress事件才搞定。再说了,XHR的用法虽然老,但网上资料多到爆,碰到问题随便搜搜就能找到解决办法,比折腾polyfill省心多了——毕竟做兼容项目,“少踩坑”比“用新东西”重要多了。
XHR和Fetch应该根据什么场景选择?
如果项目需要兼容IE11或更老的浏览器(如IE6-10),优先选XHR;如果是现代浏览器(Chrome、Edge、Firefox等),Fetch的语法更简洁,结合async/await更易读,适合用Fetch。简单说,老项目兼容选XHR,新项目现代浏览器选Fetch。
为什么Fetch请求返回500错误却不触发catch?
Fetch的Promise只有在“网络错误”(如断网、跨域配置错误)时才会reject触发catch;如果是HTTP状态码错误(如404、500),Fetch会默认resolve。解决方法是手动检查响应对象的ok
属性——如果response.ok
为false(状态码不在200-299之间),就手动抛出错误。
XHR怎么监听文件上传或下载的进度?
XHR可以通过onprogress
事件监听进度:创建XHR对象后,给xhr.onprogress
赋值一个函数,函数里的progressEvent
对象有loaded
(已传输字节数)和total
(总字节数)属性,用这两个值就能计算进度(比如loaded/total100
)。
Fetch为什么默认不带Cookie?怎么解决?
Fetch默认的credentials
配置是“same-origin”(只在同域名下带Cookie),如果需要跨域或同域都携带Cookie,要在Fetch的配置项里加credentials: 'include'
——这样请求就会自动带上Cookie,保持用户登录状态。
需要兼容IE11,还能用Fetch吗?
不能,IE11本身不支持Fetch API。如果一定要用类似Fetch的语法,可以加polyfill(比如whatwg-fetch
),但polyfill只能模拟Fetch的基本功能,像进度监听的高级特性还是不支持;更稳妥的方式是直接用XHR,毕竟XHR是IE11原生支持的。