原生 Ajax:全面了解 XMLHttpRequest (XHR)
一、XHR 概述
XMLHttpRequest (XHR) 是浏览器提供的 JavaScript API,用于在不刷新页面的情况下与服务器交换数据,是实现 Ajax 技术的核心。
1.1 XHR 的发展历程
- 早期版本:IE5/IE6 使用 ActiveXObject("Microsoft.XMLHTTP")
- 标准化:W3C 标准化后,现代浏览器都支持 XMLHttpRequest 对象
- 现代 API:Fetch API 是更现代的替代方案,但 XHR 仍被广泛支持
二、创建 XHR 对象
// 现代浏览器
const xhr = new XMLHttpRequest();
// 兼容旧版 IE (IE5/IE6)
let xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else {
// 旧版 IE
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
三、XHR 的基本使用
3.1 发送 GET 请求
function getData() {
const xhr = new XMLHttpRequest();
// 配置请求
xhr.open('GET', 'https://api.example.com/data?id=123', true);
// 设置请求头(可选)
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer token123');
// 响应处理
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log('成功:', xhr.responseText);
} else {
console.error('错误:', xhr.status);
}
}
};
// 发送请求
xhr.send();
}
3.2 发送 POST 请求
function postData() {
const xhr = new XMLHttpRequest();
const data = {
name: 'John',
age: 30
};
xhr.open('POST', 'https://api.example.com/users', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
console.log('创建成功:', xhr.responseText);
} else {
console.error('创建失败:', xhr.status);
}
}
};
// 发送 JSON 数据
xhr.send(JSON.stringify(data));
}
四、XHR 对象的重要属性和方法
4.1 属性
| 属性 |
描述 |
|---|
readyState |
请求状态:0=未初始化,1=已打开,2=已发送,3=接收中,4=完成 |
status |
HTTP 状态码(200=成功,404=未找到等) |
statusText |
HTTP 状态文本 |
responseText |
响应文本(字符串格式) |
responseXML |
响应 XML 文档(如果内容类型正确) |
response |
根据 responseType 返回响应体 |
responseType |
设置响应类型:""、"text"、"json"、"document"等 |
timeout |
请求超时时间(毫秒) |
withCredentials |
是否发送跨域凭据(cookies等) |
4.2 方法
| 方法 |
描述 |
|---|
open(method, url, async) |
初始化请求 |
send(body) |
发送请求 |
abort() |
中止请求 |
setRequestHeader(header, value) |
设置请求头 |
getResponseHeader(header) |
获取响应头 |
getAllResponseHeaders() |
获取所有响应头 |
4.3 事件
| 事件 |
描述 |
|---|
onreadystatechange |
readyState 变化时触发 |
onload |
请求完成时触发 |
onerror |
请求失败时触发 |
onprogress |
接收数据时周期性触发 |
ontimeout |
请求超时时触发 |
onloadstart |
请求开始时触发 |
onloadend |
请求结束时触发(无论成功失败) |
五、XHR 生命周期与 readyState
function trackXHRStates() {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
console.log(`readyState: ${xhr.readyState}`);
switch(xhr.readyState) {
case 0: // UNSENT
console.log('请求未初始化,open() 尚未调用');
break;
case 1: // OPENED
console.log('请求已建立,open() 已调用');
break;
case 2: // HEADERS_RECEIVED
console.log('请求已发送,send() 已调用,响应头已接收');
console.log('状态码:', xhr.status);
break;
case 3: // LOADING
console.log('响应体下载中');
console.log('已接收数据长度:', xhr.responseText.length);
break;
case 4: // DONE
console.log('请求完成');
if (xhr.status === 200) {
console.log('成功接收数据');
}
break;
}
};
xhr.open('GET', 'https://api.example.com/data', true);
xhr.send();
}
六、处理不同类型的响应
6.1 处理 JSON 响应
function getJSON() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data.json', true);
// 方法1:使用 responseType 自动解析
xhr.responseType = 'json';
xhr.onload = function() {
if (xhr.status === 200) {
// response 已经是 JavaScript 对象
console.log(xhr.response);
}
};
// 方法2:手动解析 JSON
xhr.onload = function() {
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
console.log(data);
} catch (error) {
console.error('JSON 解析错误:', error);
}
}
};
xhr.send();
}
6.2 处理 XML 响应
function getXML() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/data.xml', true);
xhr.responseType = 'document';
xhr.onload = function() {
if (xhr.status === 200) {
const xmlDoc = xhr.responseXML;
const items = xmlDoc.getElementsByTagName('item');
for (let i = 0; i < items.length; i++) {
console.log(items[i].textContent);
}
}
};
xhr.send();
}
七、高级功能
7.1 超时设置
function requestWithTimeout() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/slow-data', true);
xhr.timeout = 5000; // 5秒超时
xhr.ontimeout = function() {
console.error('请求超时');
};
xhr.onload = function() {
if (xhr.status === 200) {
console.log('数据:', xhr.responseText);
}
};
xhr.send();
}
7.2 进度监控
function trackProgress() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/large-file', true);
xhr.onprogress = function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`下载进度: ${percentComplete.toFixed(2)}%`);
}
};
xhr.onloadstart = function() {
console.log('开始下载');
};
xhr.onloadend = function() {
console.log('下载结束');
};
xhr.send();
}
7.3 上传进度监控
function uploadWithProgress() {
const xhr = new XMLHttpRequest();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', 'https://api.example.com/upload', true);
// 监控上传进度
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`上传进度: ${percentComplete.toFixed(2)}%`);
}
};
xhr.onload = function() {
if (xhr.status === 200) {
console.log('上传成功');
}
};
xhr.send(formData);
}
八、封装 XHR 为 Promise
function xhrPromise(method, url, data = null, headers = {}) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
// 设置请求头
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
try {
// 尝试解析 JSON
const response = xhr.responseText ? JSON.parse(xhr.responseText) : null;
resolve({
status: xhr.status,
data: response,
headers: xhr.getAllResponseHeaders()
});
} catch (error) {
// 如果不是 JSON,返回原始文本
resolve({
status: xhr.status,
data: xhr.responseText,
headers: xhr.getAllResponseHeaders()
});
}
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function() {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
xhr.ontimeout = function() {
reject(new Error('请求超时'));
};
// 发送数据
if (data && typeof data === 'object' && !(data instanceof FormData)) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
} else {
xhr.send(data);
}
});
}
// 使用示例
xhrPromise('GET', 'https://api.example.com/users')
.then(response => console.log('成功:', response))
.catch(error => console.error('失败:', error));
九、实际应用示例
9.1 完整的数据请求模块
class ApiService {
constructor(baseURL = '') {
this.baseURL = baseURL;
}
request(config) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const url = this.baseURL + config.url;
xhr.open(config.method || 'GET', url, true);
// 设置请求头
const headers = {
'Content-Type': 'application/json',
...config.headers
};
Object.keys(headers).forEach(key => {
xhr.setRequestHeader(key, headers[key]);
});
// 设置超时
if (config.timeout) {
xhr.timeout = config.timeout;
}
// 响应类型
if (config.responseType) {
xhr.responseType = config.responseType;
}
// 事件处理
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
let responseData;
try {
responseData = xhr.responseType === 'json'
? xhr.response
: JSON.parse(xhr.responseText);
} catch {
responseData = xhr.responseText;
}
resolve({
data: responseData,
status: xhr.status,
headers: this.parseHeaders(xhr.getAllResponseHeaders())
});
} else {
reject(this.createError(xhr));
}
};
xhr.onerror = () => reject(this.createError(xhr));
xhr.ontimeout = () => reject(new Error('请求超时'));
// 发送数据
const data = config.data || config.body;
xhr.send(data ? JSON.stringify(data) : null);
});
}
parseHeaders(headersString) {
const headers = {};
const lines = headersString.trim().split(/[\r\n]+/);
lines.forEach(line => {
const parts = line.split(': ');
const header = parts.shift();
const value = parts.join(': ');
headers[header] = value;
});
return headers;
}
createError(xhr) {
const error = new Error(`请求失败: ${xhr.status} ${xhr.statusText}`);
error.status = xhr.status;
error.statusText = xhr.statusText;
return error;
}
get(url, config = {}) {
return this.request({ ...config, method: 'GET', url });
}
post(url, data, config = {}) {
return this.request({ ...config, method: 'POST', url, data });
}
put(url, data, config = {}) {
return this.request({ ...config, method: 'PUT', url, data });
}
delete(url, config = {}) {
return this.request({ ...config, method: 'DELETE', url });
}
}
// 使用示例
const api = new ApiService('https://api.example.com');
api.get('/users')
.then(response => console.log('用户列表:', response.data))
.catch(error => console.error('错误:', error));
api.post('/users', { name: 'Alice', age: 25 })
.then(response => console.log('创建成功:', response.data))
.catch(error => console.error('错误:', error));
十、XHR 与 Fetch API 对比
| 特性 |
XMLHttpRequest |
Fetch API |
|---|
| 兼容性 |
非常好(包括 IE7+) |
较好(不兼容 IE) |
| Promise 支持 |
需要手动封装 |
原生支持 Promise |
| 请求取消 |
支持(abort()) |
支持(AbortController) |
| 进度监控 |
支持(upload/onprogress) |
有限支持(Response.body) |
| 超时设置 |
原生支持(timeout) |
需要手动实现 |
| CORS 处理 |
需要特殊处理 |
更简单直观 |
| 流式读取 |
不支持 |
支持(Response.body) |
十一、最佳实践
错误处理:始终处理 onerror 和 status 检查
超时设置:为长时间请求设置合理的超时
取消请求:在组件卸载时取消未完成的请求
进度反馈:为大文件上传/下载提供进度提示
封装重用:封装通用 XHR 函数,避免重复代码
安全考虑:验证和清理服务器响应数据
十二、总结
虽然现代开发中更推荐使用 Fetch API 或 axios 等库,但理解 XHR 的工作原理对于深入理解浏览器网络请求机制非常重要。XHR 提供了丰富的控制和事件,在某些场景下(如需要精确进度监控或更好的浏览器兼容性)仍然是合适的选择。
掌握 XHR 有助于你:
- 理解 Ajax 的工作原理
- 处理遗留代码库
- 在需要精细控制时使用
- 更好地理解现代 API(如 Fetch)的设计理念