XMLHttpRequest 是浏览器环境中非常经典的一个做 HTTP 请求的 API,虽然现在有了替代的 Fetch API 出现。但如果你要兼容就比较老旧的浏览器(比如 IE7),那么 XMLHttpRequest API 还是你的不二之选。
请求基础
XMLHttpRequest API 支持发送异步和同步请求。为了发送一个 HTTP 请求,要分 3 步:
一、创建 XMLHttpRequest 实例
var xhr = new XMLHttpRequest();
调用构造函数无需传入参数。
二、配置请求
xhr.open(method, URL, [async, user, password]);
- method:请求方法。常用的是 “GET” 和 “POST”
- URL:请求地址。一个字符串
- async:是否采用异步方式请求。默认是(
true),也可设置成同步请求(false) - user,password:如果请求需要基础认证(basic HTTP auth),就要携带这两个参数。默认为
null
xhr.open() 方法并不会打开请求,只是配置请求。
三、发送请求
xhr.send([body]);
body 表示请求体数据。GET 请求是没有请求体的,POST 请求则有。
四、监听 xhr 上的事件
请求发出去之后,为了知道请求结果,我们需要在 xhr 对象上注册事件处理器。
- load:接收到请求响应数据后触发的事件。触发这个事件,不代表请求成功——比如,HTTP 状态码可能是 400 或 500
- error:请求失败时触发的事件。比如无网络或者请求了一个无效地址(invalid URL)。
- progress:请求的响应数据下载过程中,周期性触发的一个事件。可以从回调参数中知道响应数据接收了多少。
xhr.onload = function () {connsole.log(`Loaded: ${xhr.status} ${xhr.response}`);}xhr.onerror = function () {connsole.log(`Network Error`);}xhr.onprogress = function (evt) {// evt.loaded - 下载了多少字节了// evt.lengthComputable - 一个布尔值。为 `true` 时表示进度值可计算,这也表示服务端明确发送了 Content-Length 头部。// evt.total - 一共有多少字节connsole.log(`Received: ${evt.loaded} of ${event.total}`);}
onprogress 事件的回调参数类型为 ProgressEvent,可以在 这里 看到它的详细信息。
还能通过 xhr.timeout 属性为请求指定超时时间:
// 指定超时时间为 10 秒,如果 10 秒内请求未完成,就会触发 timeout 事件xhr.timeout = 10000;
完整例子
将上面发送的步骤合起来,就得到一个完整的使用 XMLHttpRequest API 发送请求的代码。
// 1. 创建 XMLHttpRequest 实例var xhr = new XMLHttpRequest();// 2. 配置请求xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');// 3. 发送请求xhr.send();// 4. 监听请求事件xhr.onload = function () {// 判断 HTTP 返回码// 注意: this === xhr 为 `true`,响应数据就挂载在 xhr 对象上,// xhr 也是 onload 事件处理函数的上下文对象if (xhr.status == 200) {console.log(`Done, got ${xhr.response.length} bytes`) // xhr.response 为服务端响应数据} else {console.log(`Error ${xhr.status}: ${xhr.statusText}`); // 比如 404: Not Found}}xhr.onprogress = function (evt) {if (evt.lengthComputable) {console.log(`Received ${evt.loaded} of ${evt.total} bytes`);} else {console.log(`Received ${evt.loaded} bytes`); // 没有返回 `Content-Length`,不知道总字节数}}xhr.onerror = function () {console.log('Request Failed');}
示例中使用了 xhr 上几个属性,在此说明:
- status:HTTP 状态码。是个数值——
200、404、403等。在请求未发出前,或者请求失败(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 对象数据为例:
let xhr = new XMLHttpRequest();xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');// 这样获得的响应数据会经 JSON.parse() 方法处理成对象xhr.responseType = 'json';xhr.send();// 响应数据(对象): {userId: 1, id: 1, title: "delectus aut autem", completed: false}xhr.onload = function() {let responseObj = xhr.response;console.log(responseObj.userId); // 1};
xhr.readyState
我们已经介绍过获取 HTTP 返回码(xhr.status)的方式。同样,请求阶段我们也能获得——是通过 xhr.readyState 得到的。
规范中,将请求分成 5 个阶段,对应 5 个值:
- UNSET = 0 // 初始状态
- OPENED = 1 // 调用了 xhr.open 方法
- HEADERS_RECEIVED = 2 // 接收到响应头数据
- LOADING = 3 // 正在加载响应数据
- DONE = 4 // 请求完成
一个完整的请求过程,差不多会经历这样的阶段变化:0 → 1 → 2 → 3 → … → 3 → 4。响应数据在加载过程中,会定期重复触发 LOADING 阶段,对应 3 这个值。
我们使用 readystatechange 事件来跟踪请求阶段的变化:
xhr.onreadystatechange = function () {if (xhr.readyState == 3) {// 加载中}if (xhr.readyState == 4) {// 请求结束}};
在旧浏览器(比如 IE9-)中,是没有 load/error/progress 事件可供使用的。当时唯一的办法,就是通过 readystatechange 事件来跟踪请求的阶段。因此,在就脚本代码中,依然能广泛看到 readystatechange 事件的使用。
中断请求
可以使用 xhr.abort() 方法在任意时间中断请求。
xhr.abort(); // 中断请求
同步请求
前面在介绍 xhr.open 语法时,已经知道该方法的第三个参数可以用来控制发送异步/同步请求。
xhr.open(method, URL, [async, user, password]);
默认发送的异步请求(即 async 默认值为 true),将第三个参数的值设置为 false,就表示发送同步请求了。
let xhr = new XMLHttpRequest();xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', false);try {// 同步请求时,xhr.send() 方法变为同步的// 会阻塞后续代码的执行xhr.send();if (xhr.status == 200) {console.log(xhr.response);} else {console.log(`Error ${xhr.status}: ${xhr.statusText}`);}} catch(err) { // 在 catch 中捕获错误,而不用在 onerror 回调事件中alert("Request failed");}
使用 XMLHttpRequest API 发送同步请求的场景非常稀少,而且又会阻断页面渲染和响应,所以 应该谨慎使用同步请求。
除此,发送同步请求还会有诸多限制:
- 无法使用 xhr.responseType 设置响应数据类型
- 无法指定请求超时时间
- 无法跟踪请求阶段(onproress)
设置和获取头部信息
XMLHttpRequest API 还提供了设置请求头和获取响应头的方法。
设置请求头数据
语法:
xhr.setRequestHeader(name, value);
举例:
xhr.setRequestHeader('Content-Type', 'application/json');
不过有几点需要注意的是:
- 可设置的请求头是有限的,比如 Referer 和 Host 字段就不允许设置,完整的限制列表参看规范-method)。
- 头部字段一旦设置,就不可更改
- 同名字段的重新设置,不会覆盖,而是会叠加
xhr.setRequestHeader('X-Auth', '123');xhr.setRequestHeader('X-Auth', '456');// 头部变为:// X-Auth: 123, 456
获取响应头数据
获取响应头数据的方法有两个:xhr.getResponseHeader() 和 xhr.getResponseHeaders()。
语法如下:
// 获取某个响应头字段xhr.getResponseHeader(name)// 获取所有响应头字段xhr.getResponseHeaders();
有两个响应头字段是不支持获取的,一个是 Set-Cookie,还有一个是 Set-Cookie2。
这两个方法返回的结果都是字符串。
xhr.getResponseHeaders() 获取到的数据结构类似:
Cache-Control: max-age=31536000Content-Length: 4260Content-Type: image/pngDate: Sat, 08 Sep 2012 16:53:16 GMT
换行符与头部字段键值的分隔符在规范中都有明确定义:
- 行与行之间固定使用
"\r\n"作为换行符(不区分操作系统) - 字段名和字段值之间使用
": "符号(冒号后面一个空格)分隔
如果需要得到对象类型的头部字段表示,就需要自己转换下了。
var headers = xhr.getAllResponseHeaders().split('\r\n').reduce((result, current) => {let [name, value] = current.split(': ');result[name] = value;return result;}, {});/*{Cache-Control: "max-age=31536000"Content-Length: "4260"Content-Type: "image/png"Date: "Sat, 08 Sep 2012 16:53:16 GMT"} */
使用 FormData 数据发送 POST 请求
发送 POST 请求时,还支持使用内置的 FormData 对象作为请求体数据:
注意:IE10+ 浏览器才支持
FormData数据传输
语法:
var formData = new FormData([form]); // 使用现有的 <form> 表单,初始化 formData 对象formData.append(name, value); // 手动添加一个字段数据
表单数据准备好,接着就能发送请求了。
xhr.open('POST', ...)- 发送 POST 请求xhr.send(formData)- 将表单数据作为请求体数据发送
举例:
<form name="person"><input name="name" value="John"><input name="surname" value="Smith"></form><script>// 使用 <form> 表单数据初始化 FormData 对象let formData = new FormData(document.forms.person);// 再添加一个字段信息formData.append('middle', 'Lee');// 请求发出去let xhr = new XMLHttpRequest();xhr.open('POST', '/article/xmlhttprequest/post/user');xhr.send(formData);xhr.onload = () => console.log(xhr.response);</script>
使用 FormData 数据请求时,请求头会自动设置并发送 Content-Type: multipart/form-data 头部信息,确保数据经过正确的编码处理。
使用 JSON 数据发送 POST 请求
最常用的就要数这个了。我们通过设置 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);
xhr.send() 是一个非常包容的方法,可以接收多种类型的数据作为参数,发送请求体内容。除了已经介绍过的 FormData 还有 JSON 字符串,还可以是 Blob 或 BufferSource 对象。
上传进度
前面有介绍用来监听响应数据下载进度的 onprogress 事件,与之对应的就是上传进度的监控。
请求上传进度是通过 xhr.upload 对象监控的。这个对象比较特殊,没有对外暴露任何可供调用的方法,但注册了可被监听的上传事件。
xhr.upload 提供的上传事件跟 xhr 上的请求事件完全一样:
- loadstart – 开始上传
- progress – 上传过程中定期触发的事件
- abort – 取消上传
- error – 网络错误
- load – 上传成功
- timeout – 上传超时(如果设置了 timeout 属性)
- loadend – 上传结束(成功或失败)
注意:IE10+ 浏览器才支持
XMLHttpRequest.upload对象
事件监听方式如下:
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) {var xhr = new XMLHttpRequest();// 跟踪上传进度xhr.upload.onprogress = function(event) {console.log(`Uploaded ${event.loaded} of ${event.total}`);};// 上传结束(可能成功也可能失败)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 做跨源请求(cross-origin requests)时,遵循跟 Fetch API 一样的跨源资源共享策略(CORS policy)。
就是说,XMLHttpRequest 做跨域请求时,默认是不会携带凭证信息的(包括 Cookie 和 HTTP 认证信息)。为了能够携带凭证信息,需要显式将 xhr.withCredentials 属性设置为 true。
var xhr = new XMLHttpRequest();xhr.withCredentials = true;xhr.open('POST', 'http://anywhere.com/request');// ...
总结
使用 XMLHttpRequest API 发送请求总共分 4 步:
- 创建 XMLHttpRequest 实例
- 配置请求
- 发送请求
- 监听请求事件
举一个 GET 请求例子:
var xhr = new XMLHttpRequest();xhr.open('GET', '/my/url');xhr.send();xhr.onload = function() {if (xhr.status != 200) { // HTTP 请求异常?// 处理异常alert( 'Error: ' + xhr.status);return;}// 通过 xhr.response 获得请求响应};xhr.onprogress = function(event) {// 监控响应下载进度alert(`Loaded ${event.loaded} of ${event.total}`);};xhr.onerror = function() {// 处理请求错误(非 HTTP 错误,比如无网络)};
根据 最新规范,我们可以监听的事件,根据生命周期,出现顺序依次为:
- 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。
