XMLHttpRequest实战指南从基础到高级应用的完整踩坑记录
XMLHttpRequest超时处理又出问题了
昨天在项目里碰到一个诡异的问题,XMLHttpRequest请求有时候会卡住很久才报错,用户体验特别差。本来以为是个简单的timeout设置,结果折腾了一下午才搞定,还是记录一下吧。
一开始的想法太天真
我最开始的做法特别简单粗暴:
function makeRequest(url) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.timeout = 5000; // 设置5秒超时
xhr.onload = function() {
if (xhr.status === 200) {
console.log('成功', xhr.responseText);
} else {
console.log('失败', xhr.status);
}
};
xhr.ontimeout = function() {
console.log('请求超时');
};
xhr.onerror = function() {
console.log('网络错误');
};
xhr.send();
}
看起来挺正常的对吧?但问题是,有时候在网速极慢的情况下,请求还是会hang住很久才触发timeout。而且在某些移动端浏览器上,timeout的响应也不够及时。
这里我踩了个坑
折腾了半天发现,XMLHttpRequest的timeout机制其实有局限性。它只计算从send()开始到接收到响应的时间,但如果网络完全断了,或者DNS解析阶段就卡住了,timeout可能不会按预期工作。特别是移动端,网络环境复杂,经常出现这种情况。
我还发现一个问题,当用户切换到后台应用时,有些浏览器会暂停网络请求,这样timeout的时间就不准确了。比如用户切到微信聊了会天,回来的时候发现页面还在loading,但实际可能已经超时很久了。
改进版:手动控制时间
后来试了下发现,结合原生timeout事件和手动时间控制会更可靠:
function makeRequestWithBetterTimeout(url) {
const xhr = new XMLHttpRequest();
const timeoutDuration = 8000; // 8秒超时
let timeoutId;
// 设置手动超时
timeoutId = setTimeout(() => {
xhr.abort(); // 先取消请求
console.log('手动超时处理');
handleTimeout();
}, timeoutDuration);
// 原生timeout事件
xhr.timeout = timeoutDuration;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
clearTimeout(timeoutId); // 请求完成,清除定时器
if (xhr.status >= 200 && xhr.status < 300) {
handleSuccess(xhr.responseText);
} else if (xhr.status === 0) {
// 网络错误或请求被取消
handleError('Network error or cancelled');
} else {
handleError(HTTP ${xhr.status});
}
}
};
xhr.ontimeout = function() {
clearTimeout(timeoutId);
handleTimeout();
};
xhr.onerror = function() {
clearTimeout(timeoutId);
handleError('Request failed');
};
try {
xhr.send();
} catch (e) {
clearTimeout(timeoutId);
handleError('Send error: ' + e.message);
}
}
function handleSuccess(data) {
console.log('请求成功:', data);
}
function handleError(error) {
console.log('请求失败:', error);
}
function handleTimeout() {
console.log('请求超时,请检查网络连接');
}
还有个更复杂的场景
项目里还有一个特殊需求,就是需要支持重试机制。用户网络不稳定时,不能直接显示失败,要自动重试几次。这就涉及到timeout和重试的配合:
function makeRequestWithRetry(url, maxRetries = 3, retryDelay = 1000) {
let attempts = 0;
function attemptRequest() {
const xhr = new XMLHttpRequest();
const timeoutDuration = 10000;
let timeoutId;
timeoutId = setTimeout(() => {
xhr.abort();
handleAttemptFailure('timeout');
}, timeoutDuration);
xhr.timeout = timeoutDuration;
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
clearTimeout(timeoutId);
if (xhr.status >= 200 && xhr.status < 300) {
handleSuccess(xhr.responseText);
} else if (xhr.status === 0) {
// 网络错误,可能是timeout或其他网络问题
handleAttemptFailure('network');
} else {
// HTTP错误,通常是服务器问题
handleAttemptFailure('http');
}
}
};
xhr.ontimeout = function() {
clearTimeout(timeoutId);
handleAttemptFailure('timeout');
};
xhr.onerror = function() {
clearTimeout(timeoutId);
handleAttemptFailure('network');
};
try {
xhr.send();
} catch (e) {
clearTimeout(timeoutId);
handleAttemptFailure('send');
}
}
function handleAttemptFailure(type) {
attempts++;
console.log(尝试第${attempts}次失败,类型:${type});
if (attempts <= maxRetries) {
console.log(等待${retryDelay}ms后重试);
setTimeout(attemptRequest, retryDelay);
retryDelay *= 1.5; // 递增延迟时间
} else {
console.log(已重试${maxRetries}次,放弃请求);
finalFailure(type);
}
}
function handleSuccess(data) {
console.log('最终成功:', data);
// 这里处理成功的逻辑
}
function finalFailure(type) {
console.log('所有重试都失败了,类型:', type);
// 这里处理最终失败的逻辑
}
attemptRequest();
}
移动端特殊处理
后来发现在iOS Safari上有个奇葩问题,就是当设备进入休眠状态后,timeout计时可能会暂停,导致实际的超时时间比设定的要长很多。为了应对这个情况,我又加了一个页面可见性检测:
// 页面可见性变化处理
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
// 页面重新可见,可以做一些清理工作
// 比如检查是否有长时间挂起的请求
cleanupStaleRequests();
}
});
let activeRequests = new Map(); // 存储活跃的请求
function makeRequestWithVisibilityCheck(url) {
const requestId = Date.now() + Math.random();
const xhr = new XMLHttpRequest();
const timeoutDuration = 10000;
let startTime = Date.now();
// 记录活跃请求
activeRequests.set(requestId, {
xhr: xhr,
startTime: startTime
});
const timeoutId = setTimeout(() => {
xhr.abort();
activeRequests.delete(requestId);
handleTimeout();
}, timeoutDuration);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
clearTimeout(timeoutId);
activeRequests.delete(requestId);
if (xhr.status >= 200 && xhr.status < 300) {
handleSuccess(xhr.responseText);
} else {
handleError('Request failed');
}
}
};
return {
xhr: xhr,
cancel: () => {
clearTimeout(timeoutId);
xhr.abort();
activeRequests.delete(requestId);
}
};
}
function cleanupStaleRequests() {
const now = Date.now();
activeRequests.forEach((req, id) => {
if (now - req.startTime > 15000) { // 超过15秒的请求
req.xhr.abort();
activeRequests.delete(id);
}
});
}
性能考虑
上面这些方案虽然解决了问题,但也带来了一些性能上的顾虑。每次请求都创建多个定时器,频繁的abort操作是否会影响性能?经过测试发现,在正常情况下影响不大,但如果是高频率的请求场景,确实需要注意资源管理。
所以最后我又优化了一下,把定时器管理统一起来:
class RequestManager {
constructor() {
this.requests = new Map();
this.checkInterval = null;
}
addRequest(id, xhr, timeoutMs) {
this.requests.set(id, {
xhr: xhr,
startTime: Date.now(),
timeoutMs: timeoutMs
});
if (!this.checkInterval) {
this.startChecking();
}
}
removeRequest(id) {
this.requests.delete(id);
if (this.requests.size === 0 && this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
startChecking() {
this.checkInterval = setInterval(() => {
const now = Date.now();
for (let [id, req] of this.requests) {
if (now - req.startTime >= req.timeoutMs) {
req.xhr.abort();
this.removeRequest(id);
}
}
}, 1000); // 每秒检查一次
}
clear() {
this.requests.forEach(req => req.xhr.abort());
this.requests.clear();
if (this.checkInterval) {
clearInterval(this.checkInterval);
this.checkInterval = null;
}
}
}
// 使用示例
const requestManager = new RequestManager();
function makeManagedRequest(url) {
const xhr = new XMLHttpRequest();
const requestId = Symbol('request');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
requestManager.removeRequest(requestId);
// 处理响应...
}
};
requestManager.addRequest(requestId, xhr, 10000);
xhr.send();
}
踩坑提醒:这几点一定注意
总结一下踩过的几个坑:
- 单纯依赖XMLHttpRequest的timeout属性不够可靠,特别是在移动端
- abort()之后状态码是0,要区分真正的网络错误和主动取消
- 页面隐藏/显示状态下,定时器行为可能不一致
- 频繁的请求-取消操作可能影响性能,要做好资源管理
- 重试机制要考虑指数退避,避免给服务器造成压力
目前这套方案在线上跑了几个月,基本上没出现过请求hang死的情况了。虽然代码比最初版本复杂了不少,但稳定性提升很明显。网上很多教程只讲基本用法,实际项目中的复杂场景还是要自己摸索。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

暂无评论