XMLHttpRequest对象指南

本文系统性介绍 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 是一个基于状态机的对象,核心流程:

  1. 创建实例:const xhr = new XMLHttpRequest()
  2. open(method, url, async = true, username?, password?)
  3. 注册事件监听:load、error、timeout、progress、readystatechange 等
  4. 可选:setRequestHeader、withCredentials、responseType、timeout
  5. send(body?) 发送请求
  6. 监听 readyState/事件,读取 status、response 等
  7. 可选: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(暴露自定义响应头)
  • 客户端:
    • xhr.withCredentials = true 以携带 cookies/HTTP 认证
    • 服务端必须允许凭证,否则会被浏览器拦截
  • 预检(OPTIONS)请求会在使用自定义头、非简单方法或非简单内容类型时触发。

2) 缓存与条件请求

  • 浏览器可缓存 GET 响应,可用 Cache-Control、ETag、Last-Modified 控制。
  • 条件请求:
    • 设置请求头 If-None-Match: <ETag> 或 If-Modified-Since: <GMT>
    • 服务器返回 304 Not Modified 表示可用缓存。
  • 避免缓存的常用方式:给 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 证书问题等。
  • 跨域响应头读不到:
    • 服务端需要 Access-Control-Expose-Headers 暴露相应头。
  • 上传进度没有 total:
    • 服务端未提供 Content-Length 或使用分块传输;需处理 lengthComputable=false
  • JSON 解析失败:
    • 响应并非合法 JSON,或 Content-Type 错误导致误判;可用 responseType="json" 并做 fallback。
  • 下载大文件内存高:
    • 选择 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 以覆盖上传进度等特性,做到各取所长、稳健落地。