一篇文章 看完 xhr的所有用法 原理

73 阅读14分钟

状态机(State Machine)是什么?

核心思想:  一个事物在任何给定时刻只能处于有限个状态中的一个。它从一个状态转移到另一个状态的过程称为转换(Transition) 。而触发这个转换的,通常是某个事件(Event)条件(Condition)

XMLHttpRequest 对象就是一个更复杂的状态机。

  • 状态: readyState 的 0, 1, 2, 3, 4。它在任何时候都只能是这五个值之一。

  • 事件: open() 方法的调用、send() 方法的调用、从服务器接收到响应头、响应体开始下载等。

  • 转换:

    • 调用 open()(事件)使状态从 0(UNSENT)转换到 1(OPENED)。
    • 网络上接收到响应头(事件)使状态从 1(OPENED)转换到 2(HEADERS_RECEIVED)。
    • ...以此类推。

总结:状态机就是一种数学模型,用来描述一个系统在它的生命周期中如何响应不同的事件,并从一个状态转换到另一个状态。  它让复杂系统的行为变得可预测、可管理。

事件驱动(Event-Driven)是什么?

核心思想:  程序的执行流程是由事件来决定的,而不是像传统脚本那样从上到下顺序执行。

想象一个餐厅服务员:

  • 传统流程(非事件驱动) : 服务员按顺序问第一桌“要点餐吗?”,然后等他们点完。再去问第二桌“要点餐吗?”,再等他们点完... 如果第一桌的客人一直在聊天不点餐,服务员就只能干等着,整个餐厅的效率会非常低。

  • 事件驱动流程: 服务员站在那里,处于“待命”状态。

    • 事件发生: 第一桌客人招手(这是一个“点餐”事件)。
    • 事件处理: 服务员走过去,执行“记录菜单”这个任务。
    • 事件发生: 第三桌客人喊“买单”(这是一个“结账”事件)。
    • 事件处理: 服务员在点完第一桌的菜后,去给第三桌执行“结账”任务。
    • 事件发生: 厨房按铃(这是一个“上菜”事件)。
    • 事件处理: 服务员去执行“端菜”任务。

服务员的工作流程不是预先设定好的,而是由客人的各种“事件”来驱动的。程序也是如此。

在 Web 开发中:
浏览器就是一个典型的事件驱动环境。它大部分时间都在“待命”,等待各种事件发生:

  • 用户点击鼠标 (click 事件)
  • 用户滚动页面 (scroll 事件)
  • 网络请求的状态改变 (readystatechange 事件)
  • 定时器到时 (setTimeout 的回调)

你作为开发者,工作就是为这些你关心的事件注册“监听器”或“处理器” (也就是回调函数)。当事件发生时,浏览器就会调用你准备好的函数来处理它。

总结:事件驱动是一种编程范式,其中程序的流程由外部事件(如用户操作、网络活动、定时器)来主导。程序的核心是一个“事件循环(Event Loop)”,它不断地等待事件,然后分派给对应的处理函数。

XMLHttpRequest 完整状态机转换流程

状态 0: UNSENT (未发送)

  • 含义: 这是 XMLHttpRequest 对象的初始状态。对象已经被创建,但还没有调用 open() 方法进行初始化。

  • 你可以做什么: 在这个状态下,你几乎什么也做不了,除了调用 open() 方法。

  • 进入此状态:

    • const xhr = new XMLHttpRequest(); // 创建实例后,xhr.readyState 就是 0。

转换 0 → 1: open()

  • 事件: 开发者调用 xhr.open(method, url, [async], [user], [password]) 方法。
  • 动作: 你告诉了 XMLHttpRequest 对象你要请求的目标地址HTTP 方法等基本配置信息。请求还没有被发送,只是做好了准备。

状态 1: OPENED (已打开)

  • 含义: open() 方法已经被成功调用。请求的所有参数都已准备就绪,随时可以发送。

  • 你可以做什么:

    • 设置请求头:xhr.setRequestHeader('Content-Type', 'application/json');
    • 设置 responseType 等其他属性。
    • 最重要的是,调用 xhr.send() 来真正发起请求。
  • onreadystatechange 会触发吗? : 是的,从状态 0 转换到 1 时,onreadystatechange 会被第一次触发(如果已经设置了监听器)。此时 xhr.readyState 的值是 1。


转换 1 → 2: send() 与服务器响应

  • 事件:

    1. 开发者调用 xhr.send([body]) 方法。此时,请求被真正发送到网络上。
    2. 浏览器等待,直到它从服务器接收到了第一个字节的响应,也就是 HTTP 响应头 (HTTP Headers)  和 状态码 (Status Code)
  • 动作: 浏览器已经和服务器建立了连接,并且服务器告诉了浏览器这次响应的基本信息(比如 200 OK, 404 Not Found,以及 Content-Type, Content-Length 等头信息)。但响应体(真正的数据)还没有开始下载。


状态 2: HEADERS_RECEIVED (已接收到响应头)

  • 含义: send() 已被调用,并且客户端已经接收到来自服务器的完整响应头。

  • 你可以做什么:

    • 获取响应头信息:xhr.getResponseHeader('Content-Type');
    • 获取所有响应头:xhr.getAllResponseHeaders();
    • 获取 HTTP 状态码:xhr.status (例如 200)
    • 获取状态文本:xhr.statusText (例如 "OK")
  • onreadystatechange 会触发吗? : 是的,这是第二次触发。此时 xhr.readyState 的值是 2。这是非常有用的一个阶段,你可以提前知道请求是否成功(通过 status),而无需等待整个响应体下载完毕。


转换 2 → 3: 开始下载响应体

  • 事件: 浏览器开始接收响应体 (Response Body)  的数据。这个过程可能是瞬时的(对于小数据),也可能是持续一段时间的(对于大文件)。
  • 动作: 数据包正在从服务器源源不断地传输到客户端。

状态 3: LOADING (正在加载)

  • 含义: 响应体正在下载中。数据还没有完全接收完毕。

  • 你可以做什么:

    • xhr.responseText 属性此时已经可用,但它只包含已经下载的部分数据
    • 这个状态对于实现下载进度条至关重要。你可以在 onreadystatechange 事件处理器中检查 readyState 是否为 3,然后从 xhr.responseText.length 或通过监听 progress 事件来更新进度。
  • onreadystatechange 会触发吗? : 是的,这是第三次(也可能是多次)触发。对于较大的文件,readyState 停留在 3 的阶段时,onreadystatechange 可能会被触发多次。每次触发,responseText 的内容都会增加。


转换 3 → 4: 响应体下载完成

  • 事件: 浏览器已经成功接收了全部的响应体数据,或者请求失败/中止。总之,整个网络请求操作结束了。
  • 动作: 浏览器关闭了与服务器的连接(除非使用了 keep-alive)。

状态 4: DONE (完成)

  • 含义: 请求操作已完成。这不代表请求成功,只代表整个生命周期走完了。可能是成功了(HTTP 200),也可能是失败了(HTTP 404, 500),或者是网络错误。

  • 你可以做什么:

    • 这是最关键的状态。你需要在这里检查 xhr.status 来判断请求是否真的成功。
    • 如果成功(xhr.status >= 200 && xhr.status < 300),就可以安全地使用 xhr.responseText 或 xhr.response 来获取完整的数据。
    • 处理各种错误情况。
  • onreadystatechange 会触发吗? : 是的,这是最后一次触发。此时 xhr.readyState 的值是 4。绝大部分的业务逻辑都写在这个 if (xhr.readyState === 4) 的代码块里。

总结图示

codeCode

+----------------+
| 0: UNSENT      |  <-- new XMLHttpRequest()
+----------------+
       |
       | xhr.open()
       v
+----------------+
| 1: OPENED      |
+----------------+
       |
       | xhr.send() + Server sends headers
       v
+----------------+
| 2: HEADERS_RECEIVED |
+----------------+
       |
       | Browser starts downloading body
       v
+----------------+
| 3: LOADING     |  <-- (May trigger onreadystatechange multiple times)
+----------------+
       |
       | Browser finishes downloading body /or/ Request fails
       v
+----------------+
| 4: DONE        |  <-- The end of the lifecycle. Check xhr.status here.
+----------------+

通过这个完整的流程,你可以清晰地看到 XMLHttpRequest 是如何作为一个严谨的状态机工作的。它通过 readyState 这个属性,向外部暴露了其内部的每一个关键节点,而 onreadystatechange 就是那个忠实的“状态播报员”。开发者则通过在这个“播报员”函数里写 if/else 或 switch,来响应不同的状态,从而完成一次网络请求的全部逻辑。

这也反过来凸显了 fetch 基于 Promise 的设计是多么简洁:它直接隐藏了这些中间状态,只给你两个承诺——一个关于响应头(第一个 .then),一个关于响应体(第二个 .then),大大简化了开发者的心智负担。

XMLHttpRequest 上传状态机模型

这个模型描述的是 xhr.upload 对象从 xhr.send() 被调用开始,到数据完全发送到服务器为止的生命周期。

状态 0: UPLOAD_IDLE (上传空闲)

  • 含义: xhr.upload 对象已存在,但上传过程尚未开始。这是 send() 被调用之前的默认状态。

  • 你可以做什么: 在这个状态下,最重要的事情是注册事件监听器。例如:xhr.upload.addEventListener('progress', ...)。

  • 进入此状态:

    • 调用 xhr.open() 后,xhr.upload 就处于这个准备状态。
  • 与 readyState 的关系: 对应 readyState 为 1 (OPENED)。


转换 0 → 1: send() 调用

  • 事件: 开发者调用 xhr.send(data) 方法,并且 data 不为空。
  • 动作: 浏览器接管数据,准备启动网络传输。这标志着上传状态机的正式启动。上传的第一个事件 loadstart 会被触发。

状态 1: UPLOAD_STARTED (上传开始)

  • 含义: 浏览器已经开始将第一个数据包发送到网络。上传过程正式启动。

  • 对应的 xhr.upload 事件: loadstart

  • 你可以做什么:

    • 显示上传界面(如进度条)。
    • 禁用提交按钮,防止重复点击。
    • 显示状态文本:“上传开始...”。
  • loadstart 事件会触发吗? : 是的,从状态 0 转换到 1 时,loadstart 事件会被触发一次。

  • 与 readyState 的关系: 此时 readyState 仍然是 1 (OPENED)。


转换 1 → 2: 数据传输中

  • 事件: 浏览器正在网络上持续发送数据块。
  • 动作: 这个转换不是瞬时的,而是一个持续的过程。在这个过程中,progress 事件会被反复触发。

状态 2: UPLOADING (上传中)

  • 含义: 数据正在从客户端流向服务器。这是上传生命周期中最主要的工作阶段。

  • 对应的 xhr.upload 事件: progress

  • 你可以做什么:

    • 这是最重要的状态,用于更新上传进度。
    • 在 progress 事件的回调中,通过 event.loaded 和 event.total 计算百分比并更新 UI。
  • progress 事件会触发吗? : 是的,只要上传在进行,progress 事件就会被周期性地、多次地触发。

  • 与 readyState 的关系: 整个上传过程,readyState 几乎总是停留在 1 (OPENED) 。因为浏览器正忙于“说”(发送数据),还没开始“听”(接收响应)。


转换 2 → 3: 上传传输结束

  • 事件: 最后一个字节的数据已经成功发送,或者上传因错误、中止、超时而中断。
  • 动作: 客户端的数据发送任务完成。接下来会根据结束的原因触发一个终结事件(load, error, abort, timeout)。

状态 3: UPLOAD_FINISHED (上传传输完成)

  • 含义: 客户端的数据传输部分已结束。这不代表服务器已成功处理文件,仅表示数据已成功送达(或发送失败)。

  • 对应的 xhr.upload 事件: load (成功), error (失败), abort (中止), timeout (超时)。

  • 你可以做什么:

    • 如果由 load 事件进入此状态:将进度条设置为 100%,并提示“等待服务器处理...”。
    • 如果由 error 或 abort 事件进入此状态:显示错误信息,将进度条标为红色。
  • 终结事件会触发吗? : 是的,根据上传结果,load, error, abort, timeout 中的一个会被触发一次。

  • 与 readyState 的关系: 这是关键的交接点。当上传状态机进入状态 3 后,readyState 状态机才准备开始它的下一阶段:从 1 转换到 2 (HEADERS_RECEIVED),因为它现在可以开始接收服务器的响应了。


转换 3 → 4: 清理收尾

  • 事件: 无论上传是成功、失败还是中止,整个上传流程都需要一个最终的收尾信号。
  • 动作: loadend 事件被触发,标志着 xhr.upload 这条流水线的彻底终结。

状态 4: UPLOAD_ENDED (上传流程结束)

  • 含义: xhr.upload 的所有相关活动都已结束。

  • 对应的 xhr.upload 事件: loadend

  • 你可以做什么:

    • 执行最终的清理工作,例如重新启用上传按钮,隐藏进度条等。这是处理通用逻辑的最佳位置。
  • loadend 事件会触发吗? : 是的,在 load, error, abort, timeout 之后,loadend 总是会被触发一次

  • 与 readyState 的关系: 同状态 3,这是 upload 流程的终点,也是 readyState 流程继续前进的起点。

总结图示与对比

上传阶段模型核心触发事件 (xhr.upload)含义并行的 readyState
0: UPLOAD_IDLE(无)准备就绪,等待 send()1 (OPENED)
1: UPLOAD_STARTEDloadstart上传开始1 (OPENED)
2: UPLOADINGprogress (多次)数据正在传输1 (OPENED)
3: UPLOAD_FINISHEDload / error / abort数据传输结束(成功或失败)1 -> 2 的转换点
4: UPLOAD_ENDEDloadend上传流程完全终结1 -> 2 的转换点
  • xhr.upload 负责监控**请求体(Request Body)**的发送过程。
  • xhr 本身(通过 readyState 和 onreadystatechange)负责监控**响应体(Response Body)**的接收过程以及整个 HTTP 事务的状态。
  • 上传过程(xhr.upload 的生命周期)通常在 readyState 到达 2 之前就已经完成了。

XMLHttpRequest (XHR) 的概念是:一个内建于浏览器的 JavaScript 对象,它充当了一个“后台信使”,允许你的网页在不刷新整个页面的情况下,与服务器进行数据交换。

让我们把这个定义拆解成几个关键点:

  1. 它是一个 JavaScript 对象:

    • 这意味着你可以用代码来创建它 (new XMLHttpRequest()) 并控制它 (xhr.open(), xhr.send(), xhr.onload = ...)。它是一个完全由前端开发者掌控的工具。
  2. 它工作在“后台”:

    • 这意味着当 XHR 去服务器“取东西”或“送东西”时,用户当前的页面是不会被冻结或打断的。用户仍然可以滚动页面、点击按钮、输入文字。这就是所谓的异步 (Asynchronous) 。它解决了我们之前讨论的“整个页面刷新”的痛点。
  3. 它的任务是“数据交换”:

    • 这是最革命性的一点。XHR 的目标不是去服务器取回一个完整的、新的HTML页面,而是去获取纯粹的数据(通常是 JSON 格式),或者向服务器发送数据。它把“内容(数据)”和“表现(HTML/CSS)”彻底分离开来。
  4. 它实现了“局部更新”:

    • 当“信使”把数据从服务器带回来后,JavaScript 就可以接管,利用 DOM API (如 document.getElementById().innerHTML = ...),只更新页面上需要变化的一小块区域。
class Timer {
  constructor(name) {
    this.name = name;
    this.onReady = null;
  }
  start() {
    setTimeout(() => {
      if (this.onReady) {
        this.onReady();
      }
    }, 4000);
  }
}

const timer = new Timer("MyTimer");
timer.onReady = function () {
  console.log(`${this.name} is ready!`);
};
timer.start();

open(method, url, async, user, password) 异步不会造成阻塞

  • async: 我们几乎总是用 true (异步)。但了解一下同步请求 (false) 的情况:

    codeJavaScript

    // !!!强烈不推荐,仅作了解!!!
    xhr.open('GET', '/data', false); 
    xhr.send(); // JavaScript 会在这里卡住,直到请求完成
    // 只有请求完成后,下面的代码才会执行
    console.log(xhr.responseText);
    

    同步请求会冻结浏览器 UI,导致页面无响应,用户体验极差,因此应绝对避免

  • user 和 password: 用于基本的 HTTP 身份验证,会自动编码并添加到 Authorization 请求头中。

    codeJavaScript

    xhr.open('GET', '/protected-resource', true, 'myUsername', 'myPassword');
    

send(body)

send() 的参数 body 的类型取决于你要发送的数据:

  • GET/HEAD 请求:  xhr.send() 或 xhr.send(null)。
  • 发送字符串:  xhr.send('Just a string')。
  • 发送 JSON:  xhr.send(JSON.stringify({ key: 'value' }))。
  • 发送 FormData:  xhr.send(formData) (用于表单提交或文件上传)。
  • 发送 Blob/File:  xhr.send(myBlob)。
  • 发送 ArrayBuffer:  xhr.send(myArrayBuffer) (用于二进制数据)。

二、 强大的响应处理 (Response Handling)

除了 responseText,XHR 提供了更智能的方式来处理不同类型的响应数据。

xhr.responseType

这是一个至关重要的属性,必须在 send() 之前设置。它告诉 XHR 你期望服务器返回什么类型的数据,XHR 会尝试自动为你解析。

  • "" (空字符串,默认): 响应被视为文本,可以通过 xhr.responseText 获取。

  • "text": 同上。

  • "json": 超级常用!  XHR 会自动将返回的 JSON 字符串解析为 JavaScript 对象。你可以通过 xhr.response 直接访问这个对象。

    codeJavaScript

    xhr.responseType = 'json';
    xhr.onload = function() {
      // xhr.response 就是一个 JS 对象了,不再需要 JSON.parse()
      const user = xhr.response; 
      console.log(user.name); 
    };
    
  • "document": XHR 会将响应解析为一个 HTML 或 XML 文档对象,可以通过 xhr.responseXML 或 xhr.response 访问。

  • "blob": 响应被视为一个二进制大对象 (Blob),通常用于处理图片、音频、视频文件。

  • "arraybuffer": 响应被视为一个 ArrayBuffer,用于处理通用的、定长的二进制数据。

xhr.response vs xhr.responseText vs xhr.responseXML

  • xhr.response: 首选属性。它的数据类型取决于 responseType 的设置。如果是 'json',它就是对象;如果是 'blob',它就是 Blob 对象。
  • xhr.responseText: 总是返回响应的字符串形式。如果 responseType 不是 "" 或 "text",访问它可能会抛出错误。
  • xhr.responseXML: 只有当 responseType 是 "document" 且响应是有效的 XML/HTML 时才可用。

xhr.onload 是什么?

onload 是一个事件处理程序,它会在 XHR 请求成功完成时被触发。

这里的“成功完成”是一个关键概念,它具体指:

  1. 请求已经发送出去。
  2. 服务器已经返回了响应。
  3. 整个响应体(response body)已经完全下载到浏览器端。

这在技术上等同于 readyState 变为 4 (DONE) 的那一刻。

核心要点:  onload 事件的触发,只代表浏览器和服务器之间的通信过程顺利完成了。它不保证 HTTP 状态码是成功的(例如 200 OK)。一个 404 Not Found 或 500 Internal Server Error 的响应,同样会触发 onload,因为服务器确实“成功地”返回了一个完整的错误页面响应。

因此,在 onload 回调函数内部,你必须检查 xhr.status 属性来判断请求的业务逻辑是否真的成功。

如何使用 xhr.onload?

这是一个标准的使用模式,清晰且高效:

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.example.com/users/1');
xhr.responseType = 'json'; // 推荐设置,自动解析JSON

// 设置 onload 事件处理函数
xhr.onload = function() {
  // 在这里,请求已经完成,可以访问 xhr.status 和 xhr.response
  
  // 步骤1: 检查 HTTP 状态码
  if (xhr.status >= 200 && xhr.status < 300) {
    // 请求成功 (例如 200 OK, 201 Created)
    console.log('Request successful!');
    console.log('Response data:', xhr.response); 
  } else {
    // 服务器返回了错误状态 (例如 404 Not Found, 500 Server Error)
    console.error('Server returned an error.');
    console.error('Status:', xhr.status);
    console.error('Status Text:', xhr.statusText);
  }
};

// 为了健壮性,还应该处理网络层面的错误
xhr.onerror = function() {
  console.error('Network request failed. Check your connection or CORS policy.');
};

xhr.send();

三、 监控请求进度 (Progress Events) - XHR 的王牌功能

这是 XHR 相对于早期 Fetch API 的核心优势之一。你可以精细地监控数据传输的进度。

1. 监控下载进度

下载进度指的是从服务器接收数据的进度。

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('GET', '/large-video.mp4');
xhr.responseType = 'blob';

// 监听下载进度
xhr.onprogress = function(event) {
  // event.lengthComputable: 一个布尔值,表示总大小是否可知
  if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(`Downloaded ${percentComplete.toFixed(2)}%`);
    // 更新进度条
    // progressBar.value = percentComplete;
  } else {
    // 无法计算进度,因为服务器没有在响应头中发送 Content-Length
    console.log(`Received ${event.loaded} bytes`);
  }
};

xhr.onload = function() {
  console.log('Download complete.');
};

xhr.send();
  • event.loaded: 当前已传输的字节数。
  • event.total: 响应的总字节数 (需要服务器在响应头中提供 Content-Length)。

2. 监控上传进度

上传进度监控更为常用,例如在上传大文件时给用户一个实时的反馈。

关键点:  进度事件监听器需要绑定在 xhr.upload 对象上,而不是 xhr 对象本身。

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload-endpoint');

const formData = new FormData();
const largeFile = document.getElementById('fileInput').files[0];
formData.append('myFile', largeFile);

// 监听上传进度
xhr.upload.onprogress = function(event) {
  if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(`Uploaded ${percentComplete.toFixed(2)}%`);
    // uploadProgressBar.value = percentComplete;
  }
};

// 监听上传完成
xhr.upload.onload = function() {
  console.log('Upload process finished, waiting for server response...');
};

// 监听整个请求(包括上传和服务器响应)的最终完成
xhr.onload = function() {
  if (xhr.status === 200) {
    console.log('Server confirmed: Upload successful!');
  }
};

xhr.send(formData);

xhr.upload 对象也有一系列自己的事件,如 onloadstart, onprogress, onload, onerror, onabort。


🧩 一、event 是什么?

在监听进度时:

xhr.onprogress = function (event) {
  // ...
};

这里的 event 就是一个 ProgressEvent 对象
它由浏览器在数据传输过程中不断触发,告诉你目前传了多少数据。


📦 二、ProgressEvent 常见属性

属性类型说明
event.lengthComputableboolean是否能计算总大小(有的响应没有 Content-Length
event.loadednumber已经下载(或上传)的字节数
event.totalnumber总字节数(如果可计算)
event.typestring事件类型,例如 "progress", "load", "error"
event.targetXMLHttpRequest事件所属的 XHR 对象

🔍 三、属性详细说明

🧮 1. lengthComputable

表示服务器是否提供了 文件总大小信息

  • 如果是 true,说明可以用 loaded / total 计算百分比。
  • 如果是 false,说明服务器没有返回 Content-Length,无法确定进度。
if (event.lengthComputable) {
  const percent = (event.loaded / event.total) * 100;
  console.log(`下载进度:${percent.toFixed(2)}%`);
}

📊 2. loaded

表示已经 加载完成的字节数
浏览器每次接收到一部分数据就会更新这个值。


📦 3. total

表示文件的 总字节数,前提是 lengthComputabletrue


🪄 举个完整例子:

const xhr = new XMLHttpRequest();
xhr.open("GET", "/bigfile.zip", true);

xhr.onprogress = function (event) {
  if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(`Downloaded ${percentComplete.toFixed(2)}%`);
  } else {
    console.log("无法计算总大小");
  }
};

xhr.onload = function () {
  console.log("下载完成!");
};

xhr.send();

💡 四、补充:上传时的进度

上传文件时,可以监听 xhr.upload.onprogress

xhr.upload.onprogress = function (event) {
  if (event.lengthComputable) {
    const percent = (event.loaded / event.total) * 100;
    console.log(`Uploaded ${percent.toFixed(2)}%`);
  }
};

四、 控制请求 (Controlling the Request)

1. 中止请求 (abort())

在请求发出后,如果不再需要,可以随时中止它。这对于实现搜索框的自动完成(输入新字符时取消上一个请求)等功能非常有用。

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('GET', '/some-data');
xhr.send();

// 比如,用户在 1 秒后关闭了弹窗,不再需要这个数据了
setTimeout(() => {
  xhr.abort();
  console.log('Request aborted.');
}, 1000);

xhr.onabort = function() {
  console.log('The onabort event was fired.');
};

调用 abort() 后,readyState 会变为 4 (DONE),status 会变为 0。

2. 设置超时 (timeout 和 ontimeout)

如果请求在指定时间内没有完成,可以自动中止它。

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('GET', '/slow-api');

// 设置超时时间为 3 秒 (3000 毫秒)
xhr.timeout = 3000;

xhr.ontimeout = function() {
  console.error('Request timed out!');
  // 在这里可以给用户提示,或者发起重试
};

xhr.send();

五、 处理 HTTP 头 (Headers)

1. 设置请求头 (setRequestHeader())

我们已经用过它来设置 Content-Type。它可以被多次调用来设置多个头。

codeJavaScript

xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // 一个常见的标记,表明是 AJAX 请求
xhr.setRequestHeader('Accept', 'application/json');

注意:  有些头是浏览器管理的,你无法手动设置,如 Host, User-Agent 等。

2. 获取响应头

  • getResponseHeader(headerName): 获取指定的响应头的值。

    codeJavaScript

    const contentType = xhr.getResponseHeader('Content-Type');
    console.log(contentType); // "application/json; charset=utf-8"
    
  • getAllResponseHeaders(): 获取所有响应头,以一个字符串的形式返回,每行一个。

    codeJavaScript

    const allHeaders = xhr.getAllResponseHeaders();
    console.log(allHeaders);
    /*
    content-type: application/json; charset=utf-8
    date: Tue, 11 Jun 2024 10:00:00 GMT
    ...
    */
    

总结:XHR 的高级用法清单

功能分类方法/属性/事件用途
响应处理xhr.responseType预设期望的响应数据类型 (json, blob, arraybuffer 等)。
xhr.response获取根据 responseType 解析后的响应体。
进度监控xhr.onprogress监控下载进度。
xhr.upload.onprogress监控上传进度。
请求控制xhr.abort()手动中止一个正在进行的请求。
xhr.timeout设置请求的超时时间 (毫秒)。
xhr.ontimeout注册超时事件的回调函数。
头部处理xhr.setRequestHeader()设置自定义的请求头。
xhr.getResponseHeader()获取单个响应头的值。
xhr.getAllResponseHeaders()获取所有响应头的字符串。
跨域凭证xhr.withCredentials = true在跨域请求中发送 Cookies 等身份凭证。
回调函数监听对象代表的阶段触发时机
xhr.upload.onload上传阶段(客户端 → 服务器)文件成功发送完毕数据已经传给服务器了,但服务器还没回复
xhr.onload整个请求服务器响应已完全接收服务端处理完并返回结果(比如 JSON)
codeJavaScript
xhr.onload = function() {
  if (xhr.status >= 200 && xhr.status < 300) {
    // 请求成功
    console.log(xhr.response); // 使用 response 属性更佳
  } else {
    // 请求完成,但服务器返回了错误状态
    console.error('Server error:', xhr.status, xhr.statusText);
  }
};

xhr.onerror = function() {
  console.error('Network request failed');
};

5. 发送请求 (把信投进邮筒)

使用 send() 方法将请求发出去。

codeJavaScript

// 对于 GET 请求,通常没有请求体,所以传入 null 或不传参数
xhr.send(); 

// 对于 POST 请求,请求体作为参数传入
const jsonData = JSON.stringify({ name: 'Alice', age: 30 });
xhr.send(jsonData);

三、 各种实用用法详解

1. GET 请求获取 JSON 数据 (最常用)

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');

// 关键:设置 responseType 为 'json',XHR 会自动解析
xhr.responseType = 'json'; 

xhr.onload = function() {
  if (xhr.status === 200) {
    // xhr.response 现在是一个 JavaScript 对象,无需 JSON.parse()
    console.log(xhr.response); 
    // { "userId": 1, "id": 1, "title": "delectus aut autem", "completed": false }
  }
};

xhr.send();

2. POST 请求发送 JSON 数据

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://jsonplaceholder.typicode.com/posts');

// 必须设置 Content-Type,告诉服务器我们发送的是 JSON
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');

xhr.responseType = 'json';

xhr.onload = function() {
  if (xhr.status === 201) { // 201 Created 是 POST 成功的常见状态码
    console.log('Data posted successfully:', xhr.response);
  }
};

const newPost = {
  title: 'foo',
  body: 'bar',
  userId: 1,
};

// 发送时,需要将 JS 对象字符串化
xhr.send(JSON.stringify(newPost));

3. 文件上传与进度监控

这是 XHR 相比于早期 Fetch API 的一个巨大优势:原生支持进度事件

codeHtml

<input type="file" id="fileInput">
<progress id="progressBar" value="0" max="100"></progress>

codeJavaScript

const fileInput = document.getElementById('fileInput');
const progressBar = document.getElementById('progressBar');

fileInput.addEventListener('change', function(event) {
  const file = event.target.files[0];
  if (!file) return;

  const formData = new FormData();
  formData.append('myFile', file); // 'myFile' 是后端接收文件的字段名

  const xhr = new XMLHttpRequest();
  
  // 监听上传进度事件
  xhr.upload.onprogress = function(event) {
    if (event.lengthComputable) {
      const percentComplete = (event.loaded / event.total) * 100;
      progressBar.value = percentComplete;
      console.log(`Uploaded ${percentComplete.toFixed(2)}%`);
    }
  };

  xhr.onload = function() {
    if (xhr.status === 200) {
      console.log('Upload complete!');
    } else {
      console.error('Upload failed.');
    }
  };
  
  xhr.open('POST', '/upload-endpoint');
  // 使用 FormData 时,浏览器会自动设置正确的 Content-Type (multipart/form-data),
  // 所以不要手动设置 xhr.setRequestHeader('Content-Type', ...)。
  xhr.send(formData);
});

4. 处理超时

codeJavaScript

const xhr = new XMLHttpRequest();
xhr.open('GET', '/slow-resource');

// 设置超时时间为 5000 毫秒 (5秒)
xhr.timeout = 5000; 

xhr.ontimeout = function() {
  console.error('The request timed out.');
};

xhr.send();

四、 XHR vs. Fetch API

特性XMLHttpRequest (XHR)Fetch API
API 设计回调函数风格 (onload, onerror),事件驱动。Promise 风格,使用 .then(), .catch(), async/await,更现代。
易用性API 较为复杂,配置分散在多个方法和属性上。API 更简洁、语义化,链式调用清晰。
请求/响应对象请求和响应的所有信息都集中在 xhr 这一个对象上。拥有独立的 Request 和 Response 对象,设计更模块化。
错误处理onerror 处理网络层错误,HTTP 状态码错误需在 onload 中判断 xhr.status。Promise 只在网络失败时 reject。对于 404/500 等 HTTP 错误,它会 resolve,需要手动检查 response.ok 或 response.status。这是一个常见陷阱!
进度监控原生支持 onprogress (上传) 和 xhr.onprogress (下载),非常方便。原生不支持。实现进度需要借助 ReadableStream,相对复杂。
浏览器兼容性完美兼容所有浏览器,包括 IE。现代浏览器都支持,但在一些非常老的浏览器上需要 Polyfill。