这是我参与「第四届青训营」笔记创作活动的的第 3 天。
今天复习的面试知识点是 AJAX (Asynchronous JavaScript And XML,异步 JS 和 XML),指的是 JavaScript 发出的异步网络请求,可以实现页面的局部更新,使用户与页面的交互更顺畅。
AJAX
最早实现 AJAX 技术的是 XMLHttpRequest (简称 XHR),是一个浏览器的内建对象,尽管名字中有 XML,但可以操作的数据不仅限于 XML,目前开发者更多地使用 JSON 作为数据交互的格式。
现代 JavaScript 提供了 fetch API,是一种更简洁通用的 JS 异步网络请求方式,缺点是不支持旧版浏览器、不提供 XHR 的所有功能。尽管开发者应该多使用现代的 fetch API,但是目前 fetch 还无法完全代替 XHR,仍存在一些问题和局限性,比如不支持进度事件、请求超时和中止需要额外的支持等。
目前 fetch 还不是 XHR 的完美替代品,在部分情况下仍必须使用 XHR,而且 XHR 也不比 fetch 差太多,已有的使用 XHR 的应用数量也很多,比如 Vue 技术栈推荐的网络请求库 Axios 在浏览器端就是基于 XHR 的。对于开发者来说,XMLHttpRequest 和 fetch 这两种 AJAX 技术都应该掌握,根据实际情况确定使用哪一种。
这篇文章主要介绍 XMLHttpRequest 的具体用法。
XMLHttpRequest
XHR 有两种执行模式:同步 (synchronous)、异步 (asynchronous)。
异步 XHR
最常用的是异步模式。
- 配置请求
const xhr = new XMLHttpRequest();
xhr.open(method, URL, [async, user, password]);
method:HTTP 方法URL:请求的 URL,字符串或 URL 对象。如果有 search param,需要用URL对象或encodeURIComponent来确保 param 的正确编码async:默认是 true (异步模式)user和password:HTTP 基本身份验证 (basic authentication) 的用户名和密码
设置超时,以 ms 为单位,请求超时会触发 timeout 事件:
xhr.timeout = 10000; // 10 秒
设置响应类型:
"":默认,字符串类型text:字符串类型,与默认相同arraybuffer:响应类型为ArrayBufferblob:响应类型为Blobdocument:XML 文档或 HTML 文档json:响应类型为JSON(自动解析为对象)
xhr.responseType = 'json';
设置 Request Header:
xhr.setRequestHeader('Content-Type', 'application/json');
部分 Header 由浏览器管理,不可设置,比如 Referer、Host。后面设置的同名 Header 不会覆盖前面的,而是添加到值后面,以逗号 , 分隔。
xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');
// X-Auth: 123, 456
- 监听 XHR 事件
XHR 通过设置事件的回调函数来获取请求结果、处理请求错误、获取请求进度等。
XHR 事件列表 (按生命周期排序):
loadstart:请求开始progress:收到一个响应数据包abort:请求中止error:请求失败,比如网络中断或 URL 错误load:请求完成 (不考虑状态码),响应数据下载完成timeout:请求超时loadend:在abort/error/load/timeout之后触发,这 4 个事件只可能触发一个
xhr.onload = function() {
if (xhr.status !== 200) { // 分析响应的 HTTP 状态
alert(`Error ${xhr.status}: ${xhr.statusText}`); // 例如 404: Not Found
} else { // 显示结果
alert(`Done, got ${xhr.response.length} bytes`); // response 是服务器响应
}
};
xhr.onerror = function() {
alert("Request failed");
};
xhr.onprogress = function(event) {
// event.loaded —— 已经下载了多少字节
// event.lengthComputable = true,当服务器发送了 Content-Length header 时
// event.total —— 总字节数(如果 lengthComputable 为 true)
if (event.lengthComputable) {
alert(`Received ${event.loaded} of ${event.total} bytes`);
} else {
alert(`Received ${event.loaded} bytes`); // 没有 Content-Length
}
};
在 onload 处理程序中,可以访问以下属性:
status:HTTP 状态码,4xx 和 5xx 也会进入onload而不是onerrorstatusText:HTTP 状态消息response:Response Body
获取除 Set-Cookie 和 Set-Cookie2 之外的的 Response Header:
xhr.getResponseHeader('Content-Type');
// application/json
xhr.getAllResponseHeaders();
// Cache-Control: max-age=31536000
// Content-Length: 4260
// Content-Type: image/png
// Date: Sat, 08 Sep 2012 16:53:16 GMT
Header 的 name 和 value 之间严格为一个冒号一个空格 : ,换行符统一为 \r\n。
readyState
早期没有 load/error/progress 事件,只能使用 readystatechange 事件来处理请求过程,目前已经没有必要使用。
通过 xhr.readyState 访问请求状态:
UNSENT = 0:初始状态OPENED = 1:open 被调用HEADERS_RECEIVED = 2:接收到 Response HeaderLOADING = 3:正在下载响应,每接收到一个数据包重复一次状态3DONE = 4:请求完成
XHR 状态变化过程:0 → 1 → 2 → 3 → … → 3 → 4
xhr.onreadystatechange = function() {
if (xhr.readyState === 3) {
// 加载中
}
if (xhr.readyState === 4) {
// 请求完成
}
};
- 发送请求
xhr.send([body]);
body:Request Body
open 函数不会建立连接,仅配置请求,send 函数真正建立连接并发送请求。
POST 请求的 Body 可以用 FormData,Content-Type 是 multipart/form-data:
<form name="person">
<input name="name" value="John">
<input name="surname" value="Smith">
</form>
<script>
// 从表单预填充 FormData
let formData = new FormData(document.forms.person);
// 附加一个字段
formData.append("middle", "Lee");
// 发送出去
let xhr = new XMLHttpRequest();
xhr.open("POST", "/article/xmlhttprequest/post/user");
xhr.onload = () => alert(xhr.response);
xhr.send(formData);
</script>
或者用 JSON 字符串,Content-Type 是 application/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 也支持 Blob 和 BufferSource 类型。
- 中止请求
可以随时中止请求,触发 abort 事件,xhr.status 变为 0。
xhr.abort();
同步 XHR
async 参数设为 false 即为同步请求。
xhr.open(method, URL, [async, user, password]);
类似于 await promise,send 后面的代码可以访问请求完成后的 status、statusText、response,需要使用 try ... catch 来捕获请求失败的情况。
try {
xhr.send();
if (xhr.status != 200) {
alert(`Error ${xhr.status}: ${xhr.statusText}`);
} else {
alert(xhr.response);
}
} catch(err) { // 代替 onerror
alert("Request failed");
}
同步 XHR 有很多缺点,导致几乎不被使用:
- 会阻塞页面,无法响应用户的其他操作,用户体验很差
- 阻塞时间过长,浏览器会建议关闭该挂起 (hanging) 的页面
- 很多高级功能无法使用,比如跨域请求、设置超时
- 无法获取请求进度 (progress 事件)
上传进度
progress 用于获取下载进度,如果需要用 XHR 上传大文件并获取上传进度,可以使用 xhr.upload 对象,与 xhr 类似提供了若干事件:
loadstart:上传开始progress:上传了一个数据包abort:上传中止error:上传失败,比如网络中断或 URL 错误load:上传完成 (不考虑状态码)timeout:上传超时loadend:上传结束,在abort/error/load/timeout之后触发
<input type="file" onchange="upload(this.files[0])">
<script>
function upload(file) {
let 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>