2022年5月14日

XMLHttpRequest

XMLHttpRequest 是一个内置的浏览器对象,允许在 JavaScript 中发出 HTTP 请求。

尽管它的名字中包含“XML”,但它可以操作任何数据,而不仅仅是 XML 格式。我们可以上传/下载文件,跟踪进度等等。

现在,还有另一种更现代的方法 fetch,它在某种程度上取代了 XMLHttpRequest

在现代 Web 开发中,XMLHttpRequest 用于以下三个原因

  1. 历史原因:我们需要支持使用 XMLHttpRequest 的现有脚本。
  2. 我们需要支持旧浏览器,并且不想使用 polyfill(例如,为了保持脚本很小)。
  3. 我们需要 fetch 尚未提供的功能,例如跟踪上传进度。

这听起来熟悉吗?如果是,那么继续使用 XMLHttpRequest。否则,请前往 Fetch

基础知识

XMLHttpRequest 有两种操作模式:同步和异步。

让我们先看看异步模式,因为它在大多数情况下使用。

要进行请求,我们需要 3 个步骤

  1. 创建 XMLHttpRequest

    let xhr = new XMLHttpRequest();

    构造函数没有参数。

  2. 初始化它,通常在 new XMLHttpRequest 之后立即进行。

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

    此方法指定请求的主要参数

    • method – HTTP 方法。通常为 "GET""POST"
    • URL – 要请求的 URL,字符串,可以是 URL 对象。
    • async – 如果显式设置为 false,则请求为同步,我们稍后会介绍。
    • user, password – 基本 HTTP 身份验证的登录名和密码(如果需要)。

    请注意,open 调用与它的名称相反,不会打开连接。它只配置请求,但网络活动只在调用 send 时开始。

  3. 发送出去。

    xhr.send([body])

    此方法打开连接并将请求发送到服务器。可选的 body 参数包含请求主体。

    一些请求方法,如 GET,没有主体。而一些方法,如 POST,使用 body 将数据发送到服务器。我们稍后会看到这些示例。

  4. 监听 xhr 事件以获取响应。

    这三个事件是最常用的

    • load – 当请求完成时(即使 HTTP 状态为 400 或 500),并且响应已完全下载。
    • error – 当请求无法完成时,例如网络故障或 URL 无效。
    • progress – 在响应正在下载时定期触发,报告已下载了多少内容。
    xhr.onload = function() {
      alert(`Loaded: ${xhr.status} ${xhr.response}`);
    };
    
    xhr.onerror = function() { // only triggers if the request couldn't be made at all
      alert(`Network Error`);
    };
    
    xhr.onprogress = function(event) { // triggers periodically
      // event.loaded - how many bytes downloaded
      // event.lengthComputable = true if the server sent Content-Length header
      // event.total - total number of bytes (if lengthComputable)
      alert(`Received ${event.loaded} of ${event.total}`);
    };

以下是一个完整的示例。下面的代码从服务器加载 /article/xmlhttprequest/example/load 处的 URL 并打印进度

// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function() {
  if (xhr.status != 200) { // analyze HTTP status of the response
    alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
  } else { // show the result
    alert(`Done, got ${xhr.response.length} bytes`); // response is the server response
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Received ${event.loaded} of ${event.total} bytes`);
  } else {
    alert(`Received ${event.loaded} bytes`); // no Content-Length
  }

};

xhr.onerror = function() {
  alert("Request failed");
};

一旦服务器响应,我们可以在以下 xhr 属性中接收结果

status
HTTP 状态代码(数字):200404403 等,在非 HTTP 故障的情况下可以为 0
statusText
HTTP 状态消息(字符串):通常对于 200OK,对于 404Not Found,对于 403Forbidden 等等。
response(旧脚本可能使用 responseText
服务器响应主体。

我们也可以使用相应的属性指定超时时间

xhr.timeout = 10000; // timeout in ms, 10 seconds

如果请求在给定时间内未成功,则会取消请求并触发 timeout 事件。

URL 搜索参数

要向 URL 添加参数,例如 ?name=value,并确保正确编码,我们可以使用 URL 对象

let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');

// the parameter 'q' is encoded
xhr.open('GET', url); // https://google.com/search?q=test+me%21

响应类型

我们可以使用 xhr.responseType 属性设置响应格式

  • ""(默认) - 获取为字符串,
  • "text" - 获取为字符串,
  • "arraybuffer" - 获取为 ArrayBuffer(用于二进制数据,请参见章节 ArrayBuffer,二进制数组),
  • "blob" - 获取为 Blob(用于二进制数据,请参见章节 Blob),
  • "document" - 获取为 XML 文档(可以使用 XPath 和其他 XML 方法)或 HTML 文档(基于接收数据的 MIME 类型),
  • "json" - 获取为 JSON(自动解析)。

例如,让我们将响应获取为 JSON

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

xhr.responseType = 'json';

xhr.send();

// the response is {"message": "Hello, world!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Hello, world!
};
请注意

在旧脚本中,您可能还会找到 xhr.responseText 甚至 xhr.responseXML 属性。

它们出于历史原因存在,用于获取字符串或 XML 文档。如今,我们应该在 xhr.responseType 中设置格式,并像上面演示的那样获取 xhr.response

就绪状态

XMLHttpRequest 在进行过程中会处于不同的状态。当前状态可以通过 xhr.readyState 访问。

所有状态,如 规范 中所述

UNSENT = 0; // initial state
OPENED = 1; // open called
HEADERS_RECEIVED = 2; // response headers received
LOADING = 3; // response is loading (a data packet is received)
DONE = 4; // request complete

XMLHttpRequest 对象按 0123 → … → 34 的顺序遍历它们。每次通过网络接收数据包时,状态 3 都会重复。

我们可以使用 readystatechange 事件跟踪它们

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // loading
  }
  if (xhr.readyState == 4) {
    // request finished
  }
};

您可以在非常旧的代码中找到 readystatechange 监听器,它出于历史原因存在,因为曾经没有 load 和其他事件。如今,load/error/progress 处理程序已弃用它。

中止请求

我们可以随时终止请求。调用 xhr.abort() 会执行此操作

xhr.abort(); // terminate the request

这会触发abort事件,并且xhr.status变为0

同步请求

如果在open方法中,第三个参数async设置为false,则请求将同步进行。

换句话说,JavaScript执行将在send()处暂停,并在收到响应后恢复。有点像alertprompt命令。

以下是重写的示例,open的第3个参数为false

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);

try {
  xhr.send();
  if (xhr.status != 200) {
    alert(`Error ${xhr.status}: ${xhr.statusText}`);
  } else {
    alert(xhr.response);
  }
} catch(err) { // instead of onerror
  alert("Request failed");
}

这看起来可能不错,但同步调用很少使用,因为它们会阻塞页面内的JavaScript,直到加载完成。在某些浏览器中,滚动变得不可能。如果同步调用花费的时间过长,浏览器可能会建议关闭“挂起”的网页。

XMLHttpRequest的许多高级功能,例如从另一个域请求或指定超时,对于同步请求不可用。此外,如您所见,没有进度指示。

由于所有这些原因,同步请求很少使用,几乎从不使用。我们不再讨论它们。

HTTP-标头

XMLHttpRequest允许同时发送自定义标头和读取响应中的标头。

HTTP-标头有3种方法

setRequestHeader(name, value)

使用给定的namevalue设置请求标头。

例如

xhr.setRequestHeader('Content-Type', 'application/json');
标头限制

一些标头由浏览器独占管理,例如RefererHost。完整列表在规范中

为了用户安全和请求的正确性,XMLHttpRequest不允许更改它们。

无法删除标头

XMLHttpRequest的另一个特点是,无法撤消setRequestHeader

一旦设置了标头,它就被设置了。额外的调用会向标头添加信息,不会覆盖它。

例如

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// the header will be:
// X-Auth: 123, 456
getResponseHeader(name)

获取具有给定name的响应标头(除了Set-CookieSet-Cookie2)。

例如

xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()

返回所有响应标头,除了Set-CookieSet-Cookie2

标头以单行形式返回,例如

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

标头之间的换行符始终为"\r\n"(不依赖于操作系统),因此我们可以轻松地将其拆分为单个标头。名称和值之间的分隔符始终是冒号后跟一个空格": "。这是在规范中固定的。

因此,如果我们想要获取一个包含名称/值对的对象,我们需要添加一些JS。

像这样(假设如果两个标头具有相同的名称,则后面的标头会覆盖前面的标头)

let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

// headers['Content-Type'] = 'image/png'

POST,FormData

要发出POST请求,我们可以使用内置的FormData对象。

语法

let formData = new FormData([form]); // creates an object, optionally fill from <form>
formData.append(name, value); // appends a field

我们创建它,可以选择从表单中填充,如果需要,append更多字段,然后

  1. xhr.open('POST', ...) – 使用 POST 方法。
  2. xhr.send(formData) 将表单提交到服务器。

例如

<form name="person">
  <input name="name" value="John">
  <input name="surname" value="Smith">
</form>

<script>
  // pre-fill FormData from the form
  let formData = new FormData(document.forms.person);

  // add one more field
  formData.append("middle", "Lee");

  // send it out
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

表单使用 multipart/form-data 编码发送。

或者,如果我们更喜欢 JSON,那么使用 JSON.stringify 并将其作为字符串发送。

不要忘记设置 Content-Type: application/json 头部,许多服务器端框架会自动使用它解码 JSON。

let xhr = new XMLHttpRequest();

let json = JSON.stringify({
  name: "John",
  surname: "Smith"
});

xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.send(json);

.send(body) 方法非常“杂食”。它可以发送几乎任何 body,包括 BlobBufferSource 对象。

上传进度

progress 事件仅在下载阶段触发。

也就是说:如果我们 POST 了一些东西,XMLHttpRequest 首先上传我们的数据(请求体),然后下载响应。

如果我们正在上传一些大的东西,那么我们肯定更关心跟踪上传进度。但 xhr.onprogress 在这里无济于事。

还有一个对象,没有方法,专门用于跟踪上传事件:xhr.upload

它生成事件,类似于 xhr,但 xhr.upload 仅在上传时触发它们。

  • loadstart – 上传开始。
  • progress – 在上传过程中定期触发。
  • abort – 上传被中止。
  • error – 非 HTTP 错误。
  • load – 上传成功完成。
  • timeout – 上传超时(如果设置了 timeout 属性)。
  • loadend – 上传完成,无论成功还是失败。

处理程序示例

xhr.upload.onprogress = function(event) {
  alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};

xhr.upload.onload = function() {
  alert(`Upload finished successfully.`);
};

xhr.upload.onerror = function() {
  alert(`Error during the upload: ${xhr.status}`);
};

这是一个真实世界的例子:带有进度指示的文件上传

<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  // track upload progress
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);
  };

  // track completion: both successful or not
  xhr.onloadend = function() {
    if (xhr.status == 200) {
      console.log("success");
    } else {
      console.log("error " + this.status);
    }
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

跨域请求

XMLHttpRequest 可以进行跨域请求,使用与 fetch 相同的 CORS 策略。

就像 fetch 一样,它默认情况下不会将 cookie 和 HTTP 授权发送到另一个域。要启用它们,请将 xhr.withCredentials 设置为 true

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

xhr.open('POST', 'http://anywhere.com/request');
...

有关跨域标头的详细信息,请参阅 Fetch:跨域请求 章节。

总结

使用 XMLHttpRequest 的 GET 请求的典型代码

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  if (xhr.status != 200) { // HTTP error?
    // handle error
    alert( 'Error: ' + xhr.status);
    return;
  }

  // get the response from xhr.response
};

xhr.onprogress = function(event) {
  // report progress
  alert(`Loaded ${event.loaded} of ${event.total}`);
};

xhr.onerror = function() {
  // handle non-HTTP error (e.g. network down)
};

实际上还有更多事件,现代规范 列出了它们(按生命周期顺序)

  • loadstart – 请求已开始。
  • progress – 响应的数据包已到达,目前整个响应体都在 response 中。
  • abort – 请求被 xhr.abort() 调用取消。
  • error – 发生了连接错误,例如域名错误。不会发生像 404 这样的 HTTP 错误。
  • load – 请求已成功完成。
  • timeout – 请求因超时而被取消(仅在设置了超时时发生)。
  • loadend – 在 loaderrortimeoutabort 之后触发。

erroraborttimeoutload 事件是互斥的。它们中只有一个会发生。

最常用的事件是加载完成 (load)、加载失败 (error),或者我们可以使用单个 loadend 处理程序并检查请求对象 xhr 的属性以查看发生了什么。

我们已经看到了另一个事件:readystatechange。从历史上看,它很久以前就出现了,在规范确定之前。如今,没有必要使用它,我们可以用更新的事件替换它,但它经常出现在旧脚本中。

如果我们需要专门跟踪上传,那么我们应该在 xhr.upload 对象上监听相同的事件。

教程地图

评论

在评论之前阅读…
  • 如果您有改进建议,请提交 GitHub 问题或拉取请求,而不是评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标签,对于多行代码,请将它们包装在 <pre> 标签中,对于超过 10 行的代码,请使用沙箱 (plnkrjsbincodepen…)