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=31536000
Content-Length: 4260
Content-Type: image/png
Date: 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
。