XMLHttpRequest(简称XHR)是浏览器提供的JavaScript API,它允许网页在不刷新页面的情况下与服务器进行数据交换。这个功能彻底改变了Web开发的方式,使得动态内容加载和异步交互成为可能。
我第一次接触XHR是在2005年开发一个企业内部门户时,当时需要实现无刷新表单提交功能。传统的方式是使用iframe或者直接刷新页面,用户体验很差。XHR的出现让这一切变得简单优雅。
XHR本质上是一个浏览器内置的对象,它提供了在JavaScript中发送HTTP请求的能力。整个过程大致分为以下几个步骤:
javascript复制// 经典XHR使用示例
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/data', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.send();
虽然现在有Fetch API等更现代的替代方案,但XHR仍然有其独特的优势:
在实际项目中,我经常遇到需要显示文件上传进度的需求。这时候XHR的progress事件就非常有用:
javascript复制xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`上传进度: ${percent}%`);
}
};
XHR支持多种数据格式的发送和接收,这在实际开发中非常关键。以下是常见的几种情况:
javascript复制const formData = new FormData();
formData.append('username', 'john');
formData.append('avatar', fileInput.files[0]);
xhr.open('POST', '/submit');
xhr.send(formData);
javascript复制xhr.open('POST', '/api');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ key: 'value' }));
重要提示:设置自定义请求头时,某些头字段可能需要服务器配置CORS。我在实际项目中就遇到过因为漏配Access-Control-Allow-Headers导致请求失败的情况。
健壮的XHR实现必须包含完善的错误处理机制。以下是一个完整的示例:
javascript复制xhr.timeout = 5000; // 5秒超时
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
// 成功处理
} else {
// 处理HTTP错误状态码
}
};
xhr.onerror = function() {
// 处理网络错误
};
xhr.ontimeout = function() {
// 处理超时
};
xhr.onabort = function() {
// 处理取消请求
};
在实际项目中,我建议将这些错误处理逻辑封装成可复用的函数或类,而不是在每个请求中重复编写。
XHR在文件上传方面有着不可替代的优势。下面是一个完整的文件上传实现:
javascript复制function uploadFile(file) {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
// 进度监控
xhr.upload.onprogress = updateProgress;
xhr.open('POST', '/upload', true);
xhr.onload = handleSuccess;
xhr.onerror = handleError;
xhr.send(formData);
function updateProgress(e) {
if (e.lengthComputable) {
const progress = document.getElementById('progress');
progress.value = (e.loaded / e.total) * 100;
}
}
function handleSuccess() {
if (xhr.status === 200) {
console.log('上传成功');
}
}
function handleError() {
console.error('上传失败');
}
}
在WebSocket出现之前,长轮询是实现实时通信的常用技术。XHR非常适合这种场景:
javascript复制function longPoll() {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/messages', true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
processMessages(xhr.responseText);
}
// 无论成功失败都重新发起请求
setTimeout(longPoll, 1000);
}
};
xhr.send();
}
这种模式在早期的聊天应用和实时通知系统中非常常见。虽然现在有更高效的WebSocket,但在某些特殊场景下仍然有用武之地。
XHR默认遵循同源策略,跨域请求需要特殊处理。以下是几种解决方案:
javascript复制// 服务器需要设置响应头
Access-Control-Allow-Origin: *
// 或指定域名
Access-Control-Allow-Origin: https://yourdomain.com
javascript复制function handleResponse(data) {
console.log(data);
}
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleResponse';
document.body.appendChild(script);
实战经验:在IE9及以下版本中,CORS支持有限,我通常会准备一个兼容性方案,先尝试CORS,失败后回退到代理方式。
XHR对象如果处理不当可能导致内存泄漏。常见问题包括:
解决方案:
javascript复制// 在组件卸载或页面离开时取消请求
const xhr = new XMLHttpRequest();
// ...配置请求...
// 清理时
window.addEventListener('beforeunload', function() {
xhr.abort();
});
// 或者对于SPA应用
router.beforeEach((to, from, next) => {
pendingXHRs.forEach(xhr => xhr.abort());
next();
});
javascript复制// 请求队列实现示例
const MAX_CONCURRENT = 3;
const queue = [];
let activeCount = 0;
function processQueue() {
while (activeCount < MAX_CONCURRENT && queue.length) {
activeCount++;
const { xhr, resolve, reject } = queue.shift();
xhr.onload = () => {
activeCount--;
resolve(xhr.response);
processQueue();
};
xhr.onerror = reject;
xhr.send();
}
}
function enqueueRequest(xhr) {
return new Promise((resolve, reject) => {
queue.push({ xhr, resolve, reject });
processQueue();
});
}
虽然现代框架推荐使用Fetch或axios,但在某些场景下仍需直接使用XHR。以下是一些集成示例:
javascript复制class DataFetcher extends React.Component {
xhr = null;
componentDidMount() {
this.fetchData();
}
componentWillUnmount() {
if (this.xhr) {
this.xhr.abort();
}
}
fetchData = () => {
this.xhr = new XMLHttpRequest();
this.xhr.open('GET', this.props.url);
this.xhr.onload = () => {
this.props.onData(JSON.parse(this.xhr.responseText));
};
this.xhr.send();
};
render() {
return this.props.children;
}
}
javascript复制Vue.mixin({
beforeDestroy() {
if (this._xhrs) {
this._xhrs.forEach(xhr => xhr.abort());
}
},
methods: {
$xhr(config) {
const xhr = new XMLHttpRequest();
this._xhrs = this._xhrs || [];
this._xhrs.push(xhr);
return new Promise((resolve, reject) => {
xhr.open(config.method || 'GET', config.url);
if (config.headers) {
Object.keys(config.headers).forEach(key => {
xhr.setRequestHeader(key, config.headers[key]);
});
}
xhr.onload = () => resolve(JSON.parse(xhr.responseText));
xhr.onerror = reject;
xhr.send(config.data);
});
}
}
});
虽然Fetch API和axios等库提供了更现代的接口,但了解XHR仍然很重要:
在实际项目中,我通常会根据需求选择合适的工具。对于现代应用,优先使用Fetch或axios;对于需要精细控制的场景,或者维护老项目时,XHR仍然是可靠的选择。