XMLHttpRequest
是一个内置的浏览器对象,允许在 JavaScript 中发出 HTTP 请求。
尽管它的名字中包含“XML”,但它可以操作任何数据,而不仅仅是 XML 格式。我们可以上传/下载文件,跟踪进度等等。
现在,还有另一种更现代的方法 fetch
,它在某种程度上取代了 XMLHttpRequest
。
在现代 Web 开发中,XMLHttpRequest
用于以下三个原因
- 历史原因:我们需要支持使用
XMLHttpRequest
的现有脚本。 - 我们需要支持旧浏览器,并且不想使用 polyfill(例如,为了保持脚本很小)。
- 我们需要
fetch
尚未提供的功能,例如跟踪上传进度。
这听起来熟悉吗?如果是,那么继续使用 XMLHttpRequest
。否则,请前往 Fetch。
基础知识
XMLHttpRequest 有两种操作模式:同步和异步。
让我们先看看异步模式,因为它在大多数情况下使用。
要进行请求,我们需要 3 个步骤
-
创建
XMLHttpRequest
let xhr = new XMLHttpRequest();
构造函数没有参数。
-
初始化它,通常在
new XMLHttpRequest
之后立即进行。xhr.open(method, URL, [async, user, password])
此方法指定请求的主要参数
method
– HTTP 方法。通常为"GET"
或"POST"
。URL
– 要请求的 URL,字符串,可以是 URL 对象。async
– 如果显式设置为false
,则请求为同步,我们稍后会介绍。user
,password
– 基本 HTTP 身份验证的登录名和密码(如果需要)。
请注意,
open
调用与它的名称相反,不会打开连接。它只配置请求,但网络活动只在调用send
时开始。 -
发送出去。
xhr.send([body])
此方法打开连接并将请求发送到服务器。可选的
body
参数包含请求主体。一些请求方法,如
GET
,没有主体。而一些方法,如POST
,使用body
将数据发送到服务器。我们稍后会看到这些示例。 -
监听
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 状态代码(数字):
200
、404
、403
等,在非 HTTP 故障的情况下可以为0
。 statusText
- HTTP 状态消息(字符串):通常对于
200
为OK
,对于404
为Not Found
,对于403
为Forbidden
等等。 response
(旧脚本可能使用responseText
)- 服务器响应主体。
我们也可以使用相应的属性指定超时时间
xhr.timeout = 10000; // timeout in ms, 10 seconds
如果请求在给定时间内未成功,则会取消请求并触发 timeout
事件。
要向 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
对象按 0
→ 1
→ 2
→ 3
→ … → 3
→ 4
的顺序遍历它们。每次通过网络接收数据包时,状态 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()
处暂停,并在收到响应后恢复。有点像alert
或prompt
命令。
以下是重写的示例,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)
-
使用给定的
name
和value
设置请求标头。例如
xhr.setRequestHeader('Content-Type', 'application/json');
标头限制一些标头由浏览器独占管理,例如
Referer
和Host
。完整列表在规范中。为了用户安全和请求的正确性,
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-Cookie
和Set-Cookie2
)。例如
xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()
-
返回所有响应标头,除了
Set-Cookie
和Set-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
更多字段,然后
xhr.open('POST', ...)
– 使用POST
方法。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
,包括 Blob
和 BufferSource
对象。
上传进度
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
– 在load
、error
、timeout
或abort
之后触发。
error
、abort
、timeout
和 load
事件是互斥的。它们中只有一个会发生。
最常用的事件是加载完成 (load
)、加载失败 (error
),或者我们可以使用单个 loadend
处理程序并检查请求对象 xhr
的属性以查看发生了什么。
我们已经看到了另一个事件:readystatechange
。从历史上看,它很久以前就出现了,在规范确定之前。如今,没有必要使用它,我们可以用更新的事件替换它,但它经常出现在旧脚本中。
如果我们需要专门跟踪上传,那么我们应该在 xhr.upload
对象上监听相同的事件。
评论
<code>
标签,对于多行代码,请将它们包装在<pre>
标签中,对于超过 10 行的代码,请使用沙箱 (plnkr、jsbin、codepen…)