XMLHttpRequest(通常简称 XHR)是浏览器提供的原生网络请求接口。在 fetch 出现之前,几乎所有 Ajax 通信都基于它构建。即使今天前端开发普遍使用 fetch 或第三方库(如 Axios),理解 XHR 依然很有必要——它仍被大量遗留代码使用,并且可以帮助你更好地理解网络请求的底层流程。
创建 XHR 对象
使用 XMLHttpRequest 构造函数创建一个实例:
const xhr = new XMLHttpRequest();
注意,这是一个构造函数(首字母大写),每次调用都会产生一个新的请求对象。
发起请求
一次完整的 XHR 请求通常包含三个步骤:
1. 调用 open 初始化请求
xhr.open('GET', '/api/users', true);
open 方法接受三个参数:
method:HTTP 方法,如'GET'、'POST'、'PUT'等,大小写均可,通常用大写。url:请求地址,可以是相对路径或绝对地址(需符合同源策略,跨域见第16.4节)。async:布尔值,默认为true,表示异步执行;显式传入false则为同步请求(在现代开发中极少使用,下文会说明原因)。
2. 设置请求头(可选)
在有需要自定义首部时,可以在 open 之后、send 之前调用 setRequestHeader:
xhr.setRequestHeader('Content-Type', 'application/json');
该方法必须在 open 之后、send 之前调用。可以多次调用设置不同头部。
3. 调用 send 发送请求
xhr.send(null); // GET 请求一般不带消息体,传 null 即可
对于 POST 等需要携带请求体的方法,send 接受字符串、FormData、Blob 等参数:
xhr.send(JSON.stringify({ name: 'Alice' }));
监听响应:readyState 与事件
XHR 通过 readyState 属性标识请求的当前状态,其值为一个整数(0~4):
| readyState | 状态说明 |
|------------|----------|
| 0 | UNSENT:open 还未调用 |
| 1 | OPENED:open 已调用,但 send 未调用 |
| 2 | HEADERS_RECEIVED:已收到响应头 |
| 3 | LOADING:正在接收响应体(可能分块到达) |
| 4 | DONE:请求完成(无论成功还是失败) |
实际开发中,我们几乎只关心 readyState === 4 的状态。可以通过 onreadystatechange 事件监听状态变化:
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('成功', xhr.responseText);
} else {
console.error('请求失败', xhr.status);
}
}
};
现代代码中,更推荐使用 load、error、abort 等专用事件(IE 10+ 支持),逻辑更清晰:
xhr.addEventListener('load', function () {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('成功', xhr.responseText);
} else {
console.error('服务器返回错误状态码', xhr.status);
}
});
xhr.addEventListener('error', function () {
console.error('网络错误或请求被阻止');
});
xhr.addEventListener('abort', function () {
console.warn('请求已被主动取消');
});
处理响应数据
XHR 提供了多种方式读取服务端返回的数据:
xhr.responseText:字符串形式的响应数据,无论是文本、HTML 还是 JSON,都可以先拿到字符串再手动解析(例如JSON.parse(xhr.responseText))。xhr.responseXML:如果服务端明确返回Content-Type: text/xml或application/xml,浏览器会自动将响应解析为 XML 文档对象,否则该属性为null。xhr.response:通用响应属性,可通过设置xhr.responseType决定其类型:
xhr.responseType = 'json'; // 自动将响应解析为 JS 对象
xhr.responseType = 'blob'; // 二进制大文件,如图片、视频
xhr.responseType = 'arraybuffer'; // 原始二进制缓冲,用于音频处理等
当设置了 responseType 后,对应的数据会放在 xhr.response 中,而 responseText 通常会变为空字符串。因此,在获取 JSON 数据时,可以直接让浏览器帮你解析。
同步请求:了解即可,永远别用
将 open 的第三个参数设为 false 会发起同步请求。此时 send 方法会阻塞主线程,直到请求完成才继续执行:
xhr.open('GET', '/api/data', false);
xhr.send(null);
// 代码会停在这里不动,直到响应返回
console.log(xhr.responseText);
这种写法有两个致命问题:
- 阻塞整个页面:请求期间页面完全假死,用户点击、滚动全部无响应,浏览器甚至会提示“脚本无响应”。
- 已被现代规范标记为废弃:主线程上禁止使用同步 XHR(
Chrome到v80+已在主流上下文禁用)。
因此,无论出于用户体验还是未来兼容性,不要在生产代码中使用同步 XHR。
设置超时
利用 timeout 属性可以指定请求的最大等待时间(毫秒)。当超时发生时,ontimeout 事件会被触发,并且 readyState 会变为 4:
xhr.timeout = 5000; // 5 秒
xhr.ontimeout = function () {
console.error('请求超时,尝试重试或提示用户');
};
设置超时后,如果请求在指定时间内未完成,XHR 会自动将其终止。
取消请求
需要主动中止一个正在进行的请求时,调用 abort 方法:
xhr.abort();
这会触发 abort 事件(如果监听了),然后 readyState 变为 4,status 变为 0。常用于用户离开页面、输入框防抖时取消上一次请求。
完整示例
function ajax(url, { method = 'GET', data = null, headers = {}, timeout = 8000 } = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.timeout = timeout;
// 设置请求头
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
// 尝试自动解析 JSON
try {
resolve(JSON.parse(xhr.responseText));
} catch {
resolve(xhr.responseText);
}
} else {
reject({ status: xhr.status, statusText: xhr.statusText });
}
};
xhr.onerror = () => reject({ error: 'Network error' });
xhr.ontimeout = () => reject({ error: 'Timeout' });
xhr.onabort = () => reject({ error: 'Aborted' });
xhr.send(data);
});
}
// 使用
ajax('/api/user/123')
.then(data => console.log('用户数据:', data))
.catch(err => console.error('请求失败:', err));
小结与现状
XMLHttpRequest 是浏览器异步通信的功臣,但它也有一定局限性:
- API 设计偏老旧,不支持 Promise,需要手动封装。
onreadystatechange的事件模型繁琐。- 默认不携带 Cookie(需在
open配合withCredentials,且服务端需正确设置 CORS)。 - 无法直接拦截请求和响应(相比
fetch和 Axios 的拦截器)。
现代浏览器早已内置了更现代、更强大的 fetch API(第16.2节),以及在此基础上进行封装的三方库(如 Axios,第16.3节)。尽管如此,理解 XHR 仍能帮助你读懂老代码,掌握浏览器网络请求的底层运作方式,甚至在需要处理 progress 上传进度等 fetch 尚不原生支持的特性时,XHR 依然是可用的方案。