XMLHttpRequest 是浏览器环境中非常经典的一个做 HTTP 请求的 API,虽然现在有了替代的 Fetch API 出现。但如果你要兼容就比较老旧的浏览器(比如 IE7),那么 XMLHttpRequest API 还是你的不二之选。

请求基础

XMLHttpRequest API 支持发送异步和同步请求。为了发送一个 HTTP 请求,要分 3 步:

一、创建 XMLHttpRequest 实例

  1. var xhr = new XMLHttpRequest();

调用构造函数无需传入参数。

二、配置请求

  1. xhr.open(method, URL, [async, user, password]);
  1. method:请求方法。常用的是 “GET” 和 “POST”
  2. URL:请求地址。一个字符串
  3. async:是否采用异步方式请求。默认是(true),也可设置成同步请求(false
  4. user,password:如果请求需要基础认证(basic HTTP auth),就要携带这两个参数。默认为 null

xhr.open() 方法并不会打开请求,只是配置请求。

三、发送请求

  1. xhr.send([body]);

body 表示请求体数据。GET 请求是没有请求体的,POST 请求则有。

四、监听 xhr 上的事件

请求发出去之后,为了知道请求结果,我们需要在 xhr 对象上注册事件处理器。

  • load:接收到请求响应数据后触发的事件。触发这个事件,不代表请求成功——比如,HTTP 状态码可能是 400 或 500
  • error:请求失败时触发的事件。比如无网络或者请求了一个无效地址(invalid URL)。
  • progress:请求的响应数据下载过程中,周期性触发的一个事件。可以从回调参数中知道响应数据接收了多少。
  1. xhr.onload = function () {
  2. connsole.log(`Loaded: ${xhr.status} ${xhr.response}`);
  3. }
  4. xhr.onerror = function () {
  5. connsole.log(`Network Error`);
  6. }
  7. xhr.onprogress = function (evt) {
  8. // evt.loaded - 下载了多少字节了
  9. // evt.lengthComputable - 一个布尔值。为 `true` 时表示进度值可计算,这也表示服务端明确发送了 Content-Length 头部。
  10. // evt.total - 一共有多少字节
  11. connsole.log(`Received: ${evt.loaded} of ${event.total}`);
  12. }

onprogress 事件的回调参数类型为 ProgressEvent,可以在 这里 看到它的详细信息。

还能通过 xhr.timeout 属性为请求指定超时时间:

  1. // 指定超时时间为 10 秒,如果 10 秒内请求未完成,就会触发 timeout 事件
  2. xhr.timeout = 10000;

完整例子

将上面发送的步骤合起来,就得到一个完整的使用 XMLHttpRequest API 发送请求的代码。

  1. // 1. 创建 XMLHttpRequest 实例
  2. var xhr = new XMLHttpRequest();
  3. // 2. 配置请求
  4. xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
  5. // 3. 发送请求
  6. xhr.send();
  7. // 4. 监听请求事件
  8. xhr.onload = function () {
  9. // 判断 HTTP 返回码
  10. // 注意: this === xhr 为 `true`,响应数据就挂载在 xhr 对象上,
  11. // xhr 也是 onload 事件处理函数的上下文对象
  12. if (xhr.status == 200) {
  13. console.log(`Done, got ${xhr.response.length} bytes`) // xhr.response 为服务端响应数据
  14. } else {
  15. console.log(`Error ${xhr.status}: ${xhr.statusText}`); // 比如 404: Not Found
  16. }
  17. }
  18. xhr.onprogress = function (evt) {
  19. if (evt.lengthComputable) {
  20. console.log(`Received ${evt.loaded} of ${evt.total} bytes`);
  21. } else {
  22. console.log(`Received ${evt.loaded} bytes`); // 没有返回 `Content-Length`,不知道总字节数
  23. }
  24. }
  25. xhr.onerror = function () {
  26. console.log('Request Failed');
  27. }

示例中使用了 xhr 上几个属性,在此说明:

  • status:HTTP 状态码。是个数值——200404403 等。在请求未发出前,或者请求失败(onerror)的情况下,值都是 0
  • statuesText:HTTP 状态码对应的文本描述信息。常用的:200 对应 OK,404 对应 Not Found,403 对应 Forbidden
  • response(旧代码中通常使用的是 responseText):服务器响应体(即响应数据)

设置响应数据类型

响应数据类型是通过 xhr.responseType 属性设置的,默认不设置的话,通过 xhr.response 属性得到的是个字符串。

下面介绍下 xhr.responseType 属性的可取值范围:

  • ""(默认)——以字符串形式获得响应数据
  • "text"——以字符串形式获得响应数据
  • "arraybuffer"——以 ArrayBuffer 形式(二进制数据)获得响应数据
  • "blob"——以 Blob 形式(二进制数据)获得响应数据
  • "document"——以 XML 文档或 HTML 文档形式获得响应数据
  • "json"——以 JSON 对象形式获得响应数据

以最经常用到的获取 JSON 对象数据为例:

  1. let xhr = new XMLHttpRequest();
  2. xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
  3. // 这样获得的响应数据会经 JSON.parse() 方法处理成对象
  4. xhr.responseType = 'json';
  5. xhr.send();
  6. // 响应数据(对象): {userId: 1, id: 1, title: "delectus aut autem", completed: false}
  7. xhr.onload = function() {
  8. let responseObj = xhr.response;
  9. console.log(responseObj.userId); // 1
  10. };

xhr.readyState

我们已经介绍过获取 HTTP 返回码(xhr.status)的方式。同样,请求阶段我们也能获得——是通过 xhr.readyState 得到的。

规范中,将请求分成 5 个阶段,对应 5 个值:

  1. UNSET = 0 // 初始状态
  2. OPENED = 1 // 调用了 xhr.open 方法
  3. HEADERS_RECEIVED = 2 // 接收到响应头数据
  4. LOADING = 3 // 正在加载响应数据
  5. DONE = 4 // 请求完成

一个完整的请求过程,差不多会经历这样的阶段变化:0 → 1 → 2 → 3 → … → 3 → 4。响应数据在加载过程中,会定期重复触发 LOADING 阶段,对应 3 这个值。

我们使用 readystatechange 事件来跟踪请求阶段的变化:

  1. xhr.onreadystatechange = function () {
  2. if (xhr.readyState == 3) {
  3. // 加载中
  4. }
  5. if (xhr.readyState == 4) {
  6. // 请求结束
  7. }
  8. };

在旧浏览器(比如 IE9-)中,是没有 load/error/progress 事件可供使用的。当时唯一的办法,就是通过 readystatechange 事件来跟踪请求的阶段。因此,在就脚本代码中,依然能广泛看到 readystatechange 事件的使用。

中断请求

可以使用 xhr.abort() 方法在任意时间中断请求。

  1. xhr.abort(); // 中断请求

同步请求

前面在介绍 xhr.open 语法时,已经知道该方法的第三个参数可以用来控制发送异步/同步请求。

  1. xhr.open(method, URL, [async, user, password]);

默认发送的异步请求(即 async 默认值为 true),将第三个参数的值设置为 false,就表示发送同步请求了。

  1. let xhr = new XMLHttpRequest();
  2. xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', false);
  3. try {
  4. // 同步请求时,xhr.send() 方法变为同步的
  5. // 会阻塞后续代码的执行
  6. xhr.send();
  7. if (xhr.status == 200) {
  8. console.log(xhr.response);
  9. } else {
  10. console.log(`Error ${xhr.status}: ${xhr.statusText}`);
  11. }
  12. } catch(err) { // 在 catch 中捕获错误,而不用在 onerror 回调事件中
  13. alert("Request failed");
  14. }

使用 XMLHttpRequest API 发送同步请求的场景非常稀少,而且又会阻断页面渲染和响应,所以 应该谨慎使用同步请求

除此,发送同步请求还会有诸多限制:

  1. 无法使用 xhr.responseType 设置响应数据类型
  2. 无法指定请求超时时间
  3. 无法跟踪请求阶段(onproress)

设置和获取头部信息

XMLHttpRequest API 还提供了设置请求头和获取响应头的方法。

设置请求头数据

语法:

  1. xhr.setRequestHeader(name, value);

举例:

  1. xhr.setRequestHeader('Content-Type', 'application/json');

不过有几点需要注意的是:

  1. 可设置的请求头是有限的,比如 Referer 和 Host 字段就不允许设置,完整的限制列表参看规范-method)。
  2. 头部字段一旦设置,就不可更改
  3. 同名字段的重新设置,不会覆盖,而是会叠加
  1. xhr.setRequestHeader('X-Auth', '123');
  2. xhr.setRequestHeader('X-Auth', '456');
  3. // 头部变为:
  4. // X-Auth: 123, 456

获取响应头数据

获取响应头数据的方法有两个:xhr.getResponseHeader()xhr.getResponseHeaders()

语法如下:

  1. // 获取某个响应头字段
  2. xhr.getResponseHeader(name)
  3. // 获取所有响应头字段
  4. xhr.getResponseHeaders();

有两个响应头字段是不支持获取的,一个是 Set-Cookie,还有一个是 Set-Cookie2

这两个方法返回的结果都是字符串

xhr.getResponseHeaders() 获取到的数据结构类似:

  1. Cache-Control: max-age=31536000
  2. Content-Length: 4260
  3. Content-Type: image/png
  4. Date: Sat, 08 Sep 2012 16:53:16 GMT

换行符与头部字段键值的分隔符在规范中都有明确定义:

  • 行与行之间固定使用 "\r\n" 作为换行符(不区分操作系统)
  • 字段名和字段值之间使用 ": " 符号(冒号后面一个空格)分隔

如果需要得到对象类型的头部字段表示,就需要自己转换下了。

  1. var headers = xhr
  2. .getAllResponseHeaders()
  3. .split('\r\n')
  4. .reduce((result, current) => {
  5. let [name, value] = current.split(': ');
  6. result[name] = value;
  7. return result;
  8. }, {});
  9. /*
  10. {
  11. Cache-Control: "max-age=31536000"
  12. Content-Length: "4260"
  13. Content-Type: "image/png"
  14. Date: "Sat, 08 Sep 2012 16:53:16 GMT"
  15. } */

使用 FormData 数据发送 POST 请求

发送 POST 请求时,还支持使用内置的 FormData 对象作为请求体数据:

注意:IE10+ 浏览器才支持 FormData 数据传输

语法:

  1. var formData = new FormData([form]); // 使用现有的 <form> 表单,初始化 formData 对象
  2. formData.append(name, value); // 手动添加一个字段数据

表单数据准备好,接着就能发送请求了。

  1. xhr.open('POST', ...) - 发送 POST 请求
  2. xhr.send(formData) - 将表单数据作为请求体数据发送

举例:

  1. <form name="person">
  2. <input name="name" value="John">
  3. <input name="surname" value="Smith">
  4. </form>
  5. <script>
  6. // 使用 <form> 表单数据初始化 FormData 对象
  7. let formData = new FormData(document.forms.person);
  8. // 再添加一个字段信息
  9. formData.append('middle', 'Lee');
  10. // 请求发出去
  11. let xhr = new XMLHttpRequest();
  12. xhr.open('POST', '/article/xmlhttprequest/post/user');
  13. xhr.send(formData);
  14. xhr.onload = () => console.log(xhr.response);
  15. </script>

使用 FormData 数据请求时,请求头会自动设置并发送 Content-Type: multipart/form-data 头部信息,确保数据经过正确的编码处理。

使用 JSON 数据发送 POST 请求

最常用的就要数这个了。我们通过设置 Content-Type: application/json 头部信息,搭配字符串化后作为请求体内容的数据对象,就能成功发送一个 JSON 请求了。

  1. let xhr = new XMLHttpRequest();
  2. let json = JSON.stringify({
  3. name: 'John',
  4. surname: 'Smith'
  5. });
  6. xhr.open("POST", '/submit')
  7. xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
  8. xhr.send(json);

xhr.send() 是一个非常包容的方法,可以接收多种类型的数据作为参数,发送请求体内容。除了已经介绍过的 FormData 还有 JSON 字符串,还可以是 BlobBufferSource 对象。

上传进度

前面有介绍用来监听响应数据下载进度的 onprogress 事件,与之对应的就是上传进度的监控。

请求上传进度是通过 xhr.upload 对象监控的。这个对象比较特殊,没有对外暴露任何可供调用的方法,但注册了可被监听的上传事件。

xhr.upload 提供的上传事件跟 xhr 上的请求事件完全一样:

  • loadstart – 开始上传
  • progress – 上传过程中定期触发的事件
  • abort – 取消上传
  • error – 网络错误
  • load – 上传成功
  • timeout – 上传超时(如果设置了 timeout 属性)
  • loadend – 上传结束(成功或失败)

注意:IE10+ 浏览器才支持 XMLHttpRequest.upload 对象

事件监听方式如下:

  1. xhr.upload.onprogress = function(event) {
  2. alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
  3. };
  4. xhr.upload.onload = function() {
  5. alert(`Upload finished successfully.`);
  6. };
  7. xhr.upload.onerror = function() {
  8. alert(`Error during the upload: ${xhr.status}`);
  9. };

再列一个提示上传进度的例子:

  1. <input type="file" onchange="upload(this.files[0])">
  2. <script>
  3. function upload(file) {
  4. var xhr = new XMLHttpRequest();
  5. // 跟踪上传进度
  6. xhr.upload.onprogress = function(event) {
  7. console.log(`Uploaded ${event.loaded} of ${event.total}`);
  8. };
  9. // 上传结束(可能成功也可能失败)
  10. xhr.onloadend = function() {
  11. if (xhr.status == 200) {
  12. console.log("success");
  13. } else {
  14. console.log("error " + this.status);
  15. }
  16. };
  17. xhr.open("POST", "/article/xmlhttprequest/post/upload");
  18. xhr.send(file);
  19. }
  20. </script>

跨域请求

XMLHttpRequest 做跨源请求(cross-origin requests)时,遵循跟 Fetch API 一样的跨源资源共享策略(CORS policy)。

就是说,XMLHttpRequest 做跨域请求时,默认是不会携带凭证信息的(包括 Cookie 和 HTTP 认证信息)。为了能够携带凭证信息,需要显式将 xhr.withCredentials 属性设置为 true。

  1. var xhr = new XMLHttpRequest();
  2. xhr.withCredentials = true;
  3. xhr.open('POST', 'http://anywhere.com/request');
  4. // ...

总结

使用 XMLHttpRequest API 发送请求总共分 4 步:

  1. 创建 XMLHttpRequest 实例
  2. 配置请求
  3. 发送请求
  4. 监听请求事件

举一个 GET 请求例子:

  1. var xhr = new XMLHttpRequest();
  2. xhr.open('GET', '/my/url');
  3. xhr.send();
  4. xhr.onload = function() {
  5. if (xhr.status != 200) { // HTTP 请求异常?
  6. // 处理异常
  7. alert( 'Error: ' + xhr.status);
  8. return;
  9. }
  10. // 通过 xhr.response 获得请求响应
  11. };
  12. xhr.onprogress = function(event) {
  13. // 监控响应下载进度
  14. alert(`Loaded ${event.loaded} of ${event.total}`);
  15. };
  16. xhr.onerror = function() {
  17. // 处理请求错误(非 HTTP 错误,比如无网络)
  18. };

根据 最新规范,我们可以监听的事件,根据生命周期,出现顺序依次为:

  • loadstart – 请求开始
  • progress – 响应数据接收中
  • abort – 取消请求,通过调用 xhr.abort() 方法触发
  • error – 连接异常。非 HTTP 请求错误。比如请求域名错误,不包括像 404 这样的 HTTP 错误
  • load – 请求完成
  • timeout – 请求因超时结束(仅在设置了 timeout 属性后生效)
  • loadend – 请求结束。在 load、error、timeout 或 abort 事件后触发

load、error、timeout 或 abort 事件是互斥的,同一个请求结果只会触发其中一个事件。

最长用到的事件是 load 和 error,或者直接在 loadend 事件中检查 xhr 上的属性得到请求结果。

IE9- 浏览器,还没有支持上面的事件。所以只能通过监听 readystatechange 事件来得到请求结果,这是在老代码里看到是通过 readystatechange 事件监听请求结果的原因。

IE9- 浏览器,还没有支持 xhr.responseType 和 xhr.response(IE10+ 也不支持设置 xhr.responseType = 'json')。因此在老代码中会看到都是通过 xhr.responseText 属性获取响应数据,然后做对应处理的。

IE9- 浏览器,还没有支持用来监听上传事件的对象 xhr.uplaod