本文系统性介绍 XMLHttpRequest 对象:它是什么、有什么优点与特性、如何在实际项目中使用,以及常见问题与最佳实践。
一、概述:什么是 XMLHttpRequest?
- XMLHttpRequest(简称 XHR)是浏览器提供的原生 API,用于在不刷新页面的情况下向服务器发送 HTTP(S) 请求并处理响应。
- XHR 最初用于 AJAX(Asynchronous JavaScript and XML)交互,尽管名字里有 “XML”,它可以处理多种数据格式:文本、JSON、XML、二进制(ArrayBuffer/Blob)等。
- 现代替代方案是 Fetch API,但 XHR 仍广泛使用,尤其在需要上传/下载进度事件或兼容旧代码/库(如
jQuery.ajax
、Axios)时。
典型用途:
- 动态加载/提交数据(无刷新)
- 文件上传与进度展示
- 下载文件(Blob)并保存
- 轮询、条件请求(ETag/Last-Modified)
- 与表单(FormData)交互
二、核心优势与特性
优势:
- 支持异步请求,避免阻塞 UI。
- 丰富的事件模型:下载和上传进度、完成、错误、超时、取消等。
- 支持多类型响应:text、json、blob、arraybuffer、document(XML/HTML)。
- 支持设置自定义请求头、读取响应头(受安全策略限制)。
- 支持跨域请求(需服务端配合 CORS)。
- 支持带凭证请求(cookies、HTTP 认证)。
- 对缓存控制、条件请求、重试机制等可精细化掌控。
需要注意:
- 同步 XHR 已被弃用(尤其在主线程上),不建议使用。
- 与 Fetch 相比,API 复杂一些,且基于事件回调(可封装为 Promise)。
三、工作原理与生命周期
XHR 是一个基于状态机的对象,核心流程:
- 创建实例:
const xhr = new XMLHttpRequest()
open(method, url, async = true, username?, password?)
- 注册事件监听:load、error、timeout、progress、readystatechange 等
- 可选:setRequestHeader、withCredentials、responseType、timeout
send(body?)
发送请求- 监听 readyState/事件,读取 status、response 等
- 可选:
abort()
取消请求
readyState 状态:
- 0 UNSENT:未调用 open
- 1 OPENED:已调用 open
- 2 HEADERS_RECEIVED:响应头已接收
- 3 LOADING:响应体下载中
- 4 DONE:请求完成(成功或失败)
常用事件:
- readystatechange:每次 readyState 改变触发
- load:成功完成(status 在 200-299、304 通常视为成功)
- error:网络失败、CORS 拒绝等(HTTP 4xx/5xx 不会触发 error,而是触发 load,需检查 status)
- timeout:超时
- abort:被取消
- loadend:无论成功失败,结束时触发
- progress(xhr):下载进度
- progress(
xhr.upload
):上传进度
四、API 详解
主要属性:
- readyState:0-4
- status/statusText:HTTP 状态码及文本
- responseType:
""
(默认,等价于 “text”)、“arraybuffer”、“blob”、“document”、“json”、“text” - response:根据 responseType 返回不同类型
- responseText:字符串(当 responseType 为
""
或 “text” 时可用) - responseXML:XML/HTML Document(当返回可解析 XML/HTML 且类型匹配时)
- timeout:超时时间(毫秒)
- withCredentials:跨域时是否携带凭证(cookies、授权头等)
- upload:XMLHttpRequestUpload 对象(用于监听上传进度事件)
主要方法:
open(method, url, async = true, username?, password?)
setRequestHeader(name, value)
send(body?)
:body 可为字符串、FormData、Blob、ArrayBuffer、URLSearchParams 等abort()
getResponseHeader(name)
、getAllResponseHeaders()
overrideMimeType(mime)
注意:
- 有些请求头是受限的,无法通过 setRequestHeader 设置(如 Host、User-Agent、Referer 等)。
- 读取跨域响应头需服务端通过 Access-Control-Expose-Headers 显式暴露。
五、基础用法示例
1) GET 请求获取 JSON
function getJSON(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "json"; // 现代浏览器直接返回 JS 对象
xhr.timeout = 15000;
xhr.onload = () => {
// HTTP 4xx/5xx 会走 onload,需检查 status
if (xhr.status >= 200 && xhr.status < 300) {
// 某些旧浏览器不支持 responseType=json,可 fallback
const data = xhr.response ?? JSON.parse(xhr.responseText);
resolve(data);
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error("网络错误或被CORS拦截"));
xhr.ontimeout = () => reject(new Error("请求超时"));
xhr.send();
});
}
getJSON("/api/user").then(console.log).catch(console.error);
2) POST JSON
function postJSON(url, obj) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.responseType = "json";
xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response ?? JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
}
};
xhr.onerror = () => reject(new Error("网络错误"));
xhr.send(JSON.stringify(obj));
});
}
postJSON("/api/login", { username: "foo", password: "bar" });
3) 提交表单(application/x-www-form-urlencoded)
function postFormUrlEncoded(url, data) {
const body = new URLSearchParams(data).toString();
const xhr = new XMLHttpRequest();
return new Promise((resolve, reject) => {
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8");
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve(xhr.responseText) : reject(new Error(`HTTP ${xhr.status}`)));
xhr.onerror = () => reject(new Error("网络错误"));
xhr.send(body);
});
}
postFormUrlEncoded("/api/save", { a: 1, b: "x y" });
4) 文件上传(multipart/form-data)与进度
function uploadFile(url, file, extra = {}) {
return new Promise((resolve, reject) => {
const form = new FormData();
form.append("file", file); // name=后端约定字段名
Object.entries(extra).forEach(([k, v]) => form.append(k, v));
const xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
// 上传进度(xhr.upload)
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = (e.loaded / e.total) * 100;
console.log(`上传进度: ${percent.toFixed(2)}%`);
}
};
xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve(xhr.responseText) : reject(new Error(`HTTP ${xhr.status}`)));
xhr.onerror = () => reject(new Error("网络错误"));
xhr.send(form); // 无需手动设置 Content-Type,浏览器会自动生成包含 boundary 的 multipart/form-data
});
}
5) 下载文件(Blob)并保存
function downloadFile(url, filename = "download.bin") {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.onprogress = (e) => {
if (e.lengthComputable) {
console.log(`下载: ${((e.loaded / e.total) * 100).toFixed(2)}%`);
}
};
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
const urlObj = URL.createObjectURL(xhr.response);
const a = document.createElement("a");
a.href = urlObj;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(urlObj);
} else {
console.error("下载失败", xhr.status);
}
};
xhr.onerror = () => console.error("网络错误");
xhr.send();
}
6) 读取自定义响应头
const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data", true);
xhr.onload = () => {
if (xhr.status === 200) {
// 跨域时,服务端须通过 Access-Control-Expose-Headers 暴露该头
const etag = xhr.getResponseHeader("ETag");
console.log("ETag:", etag);
}
};
xhr.send();
六、进阶主题
1) CORS 与凭证
- 跨域请求需要服务端设置 CORS 响应头:
- Access-Control-Allow-Origin:
https://example.com
(不能与 Credentials 一起用*
) - Access-Control-Allow-Credentials: true(若携带凭证)
- Access-Control-Allow-Methods/Access-Control-Allow-Headers(预检)
- Access-Control-Expose-Headers(暴露自定义响应头)
- Access-Control-Allow-Origin:
- 客户端:
xhr.withCredentials = true
以携带 cookies/HTTP 认证- 服务端必须允许凭证,否则会被浏览器拦截
- 预检(OPTIONS)请求会在使用自定义头、非简单方法或非简单内容类型时触发。
2) 缓存与条件请求
- 浏览器可缓存 GET 响应,可用 Cache-Control、ETag、Last-Modified 控制。
- 条件请求:
- 设置请求头 If-None-Match:
<ETag>
或 If-Modified-Since:<GMT>
- 服务器返回 304 Not Modified 表示可用缓存。
- 设置请求头 If-None-Match:
- 避免缓存的常用方式:给 URL 添加时间戳或版本号参数,例如
?_t=Date.now()
(仅在明确需要绕过缓存时使用)。
3) 错误处理与状态码
- xhr.onerror:仅网络层错误或 CORS 拦截;HTTP 4xx/5xx 不会触发 error。
- 在 onload 中检查
xhr.status
:- 2xx:成功
- 304:可视为成功(缓存命中)
- 0:常见于请求被取消、跨域被拦截、
file://
、离线等
- 建议统一封装:只在 200-299、304 视为成功,其余 reject。
4) 超时与取消
- 超时:
xhr.timeout = 15000
;监听xhr.ontimeout
。 - 取消:
xhr.abort()
;会触发 abort 和 loadend。 - 可封装返回
{ promise, abort }
,以实现可取消请求(见实战封装)。
5) 上传与下载进度
- 下载进度:
xhr.onprogress
,事件对象e.lengthComputable
/e.loaded
/e.total
。 - 上传进度:
xhr.upload.onprogress
。 - 显示进度条时需处理 lengthComputable 为 false 的情况(服务器未提供 Content-Length)。
6) 处理二进制数据
- 设置
responseType = "arraybuffer"
或"blob"
。 - ArrayBuffer 可用于解析自定义二进制协议、音视频解封装、加密等。
- Blob 适用于文件下载、生成 URL 对象供
<img>/<video>
或a[download]
使用。
7) 解析 XML
const xhr = new XMLHttpRequest();
xhr.open("GET", "/feed.xml");
xhr.responseType = "document"; // 或保持默认并用 responseXML
xhr.overrideMimeType("text/xml"); // 某些服务器未正确返回类型时可强制
xhr.onload = () => {
const doc = xhr.responseXML || xhr.response; // Document
const titles = doc.querySelectorAll("item > title");
titles.forEach(n => console.log(n.textContent));
};
xhr.send();
8) 自定义请求头与受限头
- 可设置的常见头:Accept、Content-Type、X-Requested-With、Authorization(跨域时服务端需允许)。
- 受限头不可设置:Host、Connection、Referer、User-Agent、Content-Length 等(由浏览器管理)。
- 跨域读取响应头需服务端使用 Access-Control-Expose-Headers 暴露。
9) 身份认证与 cookie
- open 支持用户名/密码参数用于 HTTP 基本认证:
xhr.open("GET", url, true, user, pass)
withCredentials = true
可携带 cookie;服务端需设置Access-Control-Allow-Credentials: true
且不能用*
作为 Allow-Origin。
10) 渐进式接收(readyState=3)
- 文本响应在 LOADING(3)阶段可以通过 responseText 增量读取部分数据,适用于简单 SSE-like 流式文本。
- 注意分片边界与编码问题;现代流式需求更推荐 Fetch + ReadableStream。
七、实战封装:Promise + 取消 + 进度
function xhrRequest({
method = "GET",
url,
headers = {},
data = null,
responseType = "json",
timeout = 15000,
withCredentials = false,
onDownloadProgress,
onUploadProgress,
} = {}) {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.responseType = responseType;
xhr.timeout = timeout;
xhr.withCredentials = withCredentials;
for (const [k, v] of Object.entries(headers)) {
xhr.setRequestHeader(k, v);
}
if (onDownloadProgress) {
xhr.onprogress = (e) => onDownloadProgress(e);
}
if (onUploadProgress) {
xhr.upload.onprogress = (e) => onUploadProgress(e);
}
const promise = new Promise((resolve, reject) => {
xhr.onload = () => {
const ok = (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304;
if (!ok) {
reject(new Error(`HTTP ${xhr.status}: ${xhr.statusText}`));
return;
}
try {
if (responseType === "json") {
// 兼容:某些环境 responseType=json 不可用
const result = xhr.response ?? JSON.parse(xhr.responseText);
resolve(result);
} else {
resolve(xhr.response ?? xhr.responseText);
}
} catch (e) {
reject(e);
}
};
xhr.onerror = () => reject(new Error("网络错误或CORS拦截")));
xhr.ontimeout = () => reject(new Error("请求超时")));
xhr.onabort = () => reject(new Error("请求已取消")));
});
const send = () => xhr.send(data);
const abort = () => xhr.abort();
// 自动推断 Content-Type(仅在未手动设置时)
const hasCT = Object.keys(headers).some(h => h.toLowerCase() === "content-type");
if (!hasCT && data && typeof data === "string") {
xhr.setRequestHeader("Content-Type", "text/plain;charset=UTF-8");
}
// FormData/Blob/ArrayBuffer 由浏览器处理类型;JSON 建议显式设置
// URLSearchParams 会自动使用 application/x-www-form-urlencoded
send();
return { promise, abort, xhr };
}
// 使用示例
const { promise, abort } = xhrRequest({
method: "POST",
url: "/api/upload",
data: (() => { const f = new FormData(); f.append("file", fileInput.files[0]); return f; })(),
onUploadProgress: (e) => {
if (e.lengthComputable) console.log(`上传:${(e.loaded / e.total * 100).toFixed(2)}%`);
},
});
promise.then(console.log).catch(console.error);
// 在需要时取消:abort();
八、最佳实践与反模式
最佳实践:
- 始终处理 onload 中的 status 检查,区分网络错误与 HTTP 错误。
- 设置合理的 timeout,并在失败时给出可重试的 UI。
- 需要跨域且带 cookie 时,withCredentials 与服务端 CORS 配置匹配。
- 上传/下载提供进度反馈,处理
lengthComputable=false
的情况。 - 使用 FormData 发送文件/表单,不要手动拼装 multipart 边界。
- 解析 JSON 时考虑 try/catch,或使用
responseType="json"
。 - 使用 ETag/Last-Modified 实现条件请求,节省带宽。
- 设计通用封装,暴露取消(abort)能力,集中处理错误与重试。
- 生产环境启用 HTTPS,避免敏感信息明文传输。
避免事项:
- 避免同步 XHR(在主线程上已弃用,会卡 UI,部分环境被禁止)。
- 避免无意义的频繁轮询;可考虑服务器推送或合理的退避策略。
- 避免在 DOM 中直接注入未转义的响应文本(防 XSS)。
- 避免设置受限请求头;避免跨域读取未暴露的响应头。
- 避免在大二进制下载时使用 responseText(应使用 blob/arraybuffer)。
九、与 Fetch/Axios 等的对比与选择
- Fetch API:
- 优点:基于 Promise,语义更现代,支持流式下载(ReadableStream)、拦截更灵活(通过自封装),与 Service Worker 等生态契合。
- 局限:标准上缺少上传进度事件(下载可流式读取),取消需 AbortController(优雅),处理超时需手动 race。
- XMLHttpRequest:
- 优点:上传与下载进度事件简单直接;兼容旧代码;细粒度控制。
- 局限:回调风格(需封装)、API 繁琐、同步模式已弃用。
- Axios:
- 浏览器端基于 XHR(Node 端基于 http),提供 Promise、拦截器、转换器、取消、超时、JSON 自动处理,更易用。
- 如不想手写封装,优先选 Axios 或直接用 Fetch。
选择建议:
- 需要上传进度:XHR 或 Axios(基于 XHR)。
- 需要下载流式解析:Fetch。
- 普通 JSON API:Fetch 或 Axios;已有遗留代码或库:XHR 也可。
十、常见问题与排错
- 状态码是 0:
- 被取消(abort)、网络离线、跨域被拦截、
file://
协议访问、HTTPS 证书问题等。
- 被取消(abort)、网络离线、跨域被拦截、
- 跨域响应头读不到:
- 服务端需要 Access-Control-Expose-Headers 暴露相应头。
- 上传进度没有 total:
- 服务端未提供 Content-Length 或使用分块传输;需处理
lengthComputable=false
。
- 服务端未提供 Content-Length 或使用分块传输;需处理
- JSON 解析失败:
- 响应并非合法 JSON,或 Content-Type 错误导致误判;可用
responseType="json"
并做 fallback。
- 响应并非合法 JSON,或 Content-Type 错误导致误判;可用
- 下载大文件内存高:
- 选择
responseType="blob"
,且尽快释放 URL 对象(URL.revokeObjectURL
)。
- 选择
- 设置了 Content-Type 却出错:
- 使用 FormData 时不要手动设置 Content-Type,浏览器会自动带 boundary;手动设置反而破坏格式。
- 自定义请求头跨域失败:
- 预检请求未通过,服务端需要将该头加入 Access-Control-Allow-Headers。
十一、扩展话题
- 条件与并发控制:结合 ETag、If-None-Match、If-Modified-Since 与并发请求队列管理。
- 指数退避重试:网络不稳定时对幂等请求使用 1s、2s、4s… 的退避重试策略,避免拥堵。
- 单页应用路由与中断:路由切换时调用 abort() 取消无效请求,避免内存泄漏与状态污染。
- 测试与 mock:
- 使用
Sinon.js
的 fake XHR 或测试框架的 mock 能力模拟响应。
- 使用
- 安全:
- CSRF 防护需配合服务端策略(SameSite cookies、CSRF Token、Origin/Referer 校验);XHR 自身不解决 CSRF。
- XSS 防护:对服务端返回的可变内容进行转义或使用安全渲染方案。
十二、快捷参考(代码片段)
- 设置 CORS 凭证:
xhr.withCredentials = true;
- 超时与处理:
xhr.timeout = 10000;
xhr.ontimeout = () => { /* ... */ };
- 读取所有响应头:
console.log(xhr.getAllResponseHeaders()); // 字符串,每行 "key: value"
- 取消请求:
xhr.abort();
- 基本认证:
xhr.open("GET", url, true, "user", "pass");
结语
XMLHttpRequest 依然是前端网络请求的重要基石。在需要进度事件、与旧系统集成或特定兼容要求时,XHR 是可靠选择。结合合理的封装、错误处理、CORS 配置与缓存策略,XHR 能满足从简单数据拉取到文件传输的广泛需求。对于新项目,优先考虑 Fetch/Axios,在必要时引入 XHR 以覆盖上传进度等特性,做到各取所长、稳健落地。