# 网络请求-XHR
# XMLHttpRequest
XMLHttpRequest 是一个内建的浏览器对象,它允许使用 JavaScript 发送 HTTP 请求。
虽然它的名字里面有 “XML” 一词,但它可以操作任何数据,而不仅仅是 XML 格式。我们可以用它来上传/下载文件,跟踪进度等。
# XMLHttpRequest 基础
要发送请求,需要 3 个步骤:
- 创建 XMLHttpRequest:
let xhr = new XMLHttpRequest();
- 初始化它,通常就在 new XMLHttpRequest 之后:
xhr.open(method, URL, [async, user, password]);
- 发送请求。
xhr.send([body]);
随后我们可以监听 xhr 事件以获取响应
xhr.onload = function () {
console.log(`Loaded: ${xhr.status} ${xhr.response}`);
};
// 跟踪下载进度
xhr.onprogress = function (event) {
console.log(`Received ${event.loaded} of ${event.total}`);
};
xhr.onerror = function () {
// 仅在根本无法发出请求时触发
console.log(`Network Error`);
};
# 响应类型
我们可以使用 xhr.responseType 属性来设置响应格式:
- ""
- "text"
- "arrayBuffer"
- "blob"
- "document"
- "json"
例如,我们以 JSON 格式获取响应:
let xhr = new XMLHttpRequest();
xhr.open("GET", "/article/xmlhttprequest/example/json");
xhr.responseType = "json";
xhr.send();
// 响应为 {"message": "Hello, world!"}
xhr.onload = function () {
let responseObj = xhr.response;
alert(responseObj.message); // Hello, world!
};
# readyState
XMLHttpRequest 的状态(state)会随着它的处理进度变化而变化。可以通过 xhr.readyState 来了解当前状态。
规范 中提到的所有状态如下:
UNSENT = 0; // 初始状态
OPENED = 1; // open 被调用
HEADERS_RECEIVED = 2; // 接收到 response header
LOADING = 3; // 响应正在被加载(接收到一个数据包)
DONE = 4; // 请求完成
# abort
我们可以随时终止请求。调用 xhr.abort() 即可:
xhr.abort(); // 终止请求
它会触发 abort 事件,且 xhr.status 变为 0。
# 同步请求
如果在 open 方法中将第三个参数 async 设置为 false,那么请求就会以同步的方式进行。
这看起来好像不错,但是很少使用同步调用,因为它们会阻塞页面内的 JavaScript,直到加载完成。
# header
XMLHttpRequest 允许发送自定义 header,并且可以从响应中读取 header。
// setRequestHeader设置请求Header
xhr.setRequestHeader("Content-Type", "application/json");
// getResponseHeader获取响应Header
xhr.getResponseHeader("Content-Type");
// getAllResponseHeaders返回除 Set-Cookie* 外的所有 response header
let headers = xhr
.getAllResponseHeaders()
.split("\r\n")
.reduce((result, current) => {
let [name, value] = current.split(": ");
result[name] = value;
return result;
}, {});
# 上传进度
progress 事件仅在下载阶段触发。
如果我们要上传的东西很大,那么我们肯定会对跟踪上传进度感兴趣。但是 xhr.onprogress 在这里并不起作用。
这里有另一个对象,它没有方法,它专门用于跟踪上传事件:xhr.upload。
它会生成事件,类似于 xhr,但是 xhr.upload
仅在上传时触发它们
<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>
# 凭据
就像 fetch 一样,默认情况下不会将 cookie 和 HTTP 授权发送到其他域。要启用它们,可以将 xhr.withCredentials 设置为 true:
let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
# 总结
// 允许携带凭据信息
xhr.withCredentials = true;
// 跟踪上传进度
xhr.upload.onprogress = function (event) {
console.log(`Uploaded ${event.loaded} of ${event.total}`);
};
// 跟踪下载进度
xhr.onprogress = function (event) {
console.log(`Received ${event.loaded} of ${event.total}`);
};
# 可恢复的文件上传
连接断开后如何恢复上传?这里没有对此的内建选项,但是我们有实现它的一些方式。
# 不太实用的进度事件
我们有 xhr.upload.onprogress 来跟踪上传进度。
不幸的是,它不会帮助我们在此处恢复上传,因为它会在数据 被发送 时触发,但是服务器是否接收到了?浏览器并不知道。
这就是为什么此事件仅适用于显示一个好看的进度条。
要恢复上传,我们需要 确切地 知道服务器接收的字节数。而且只有服务器能告诉我们,因此,我们将发出一个额外的请求。
# 自定义算法实现
首先,创建一个文件 id,以唯一地标识我们要上传的文件
let fileId = file.name + "-" + file.size + "-" + +file.lastModifiedDate;
向服务器发送一个请求,询问它已经有了多少字节,像这样
let response = await fetch("status", {
headers: {
"X-File-Id": fileId
}
});
// 服务器已有的字节数
let startByte = +(await response.text());
然后,我们可以使用 Blob 和 slice 方法来发送从 startByte 开始的文件
xhr.open("POST", "upload", true);
// 文件 id,以便服务器知道我们要恢复的是哪个文件
xhr.setRequestHeader("X-File-Id", fileId);
// 发送我们要从哪个字节开始恢复,因此服务器知道我们正在恢复
xhr.setRequestHeader("X-Start-Byte", startByte);
// ...
// 文件可以是来自 input.files[0],或者另一个源
xhr.send(file.slice(startByte));
# 轮询
轮询是与服务器保持持久连接的最简单的方式,它很容易实现,在很多场景下也很好用。
# 常规轮询
从服务器获取新信息的最简单的方式是定期轮询。例如,每 10 秒一次。
这可行,但是也有些缺点:
- 消息传递的延迟最多为 10 秒(两个请求之间)。
- 即使没有消息,服务器也会每隔 10 秒被请求轰炸一次,就性能而言,这是一个很大的负担。
# 长轮询
所谓“长轮询”是轮询服务器的一种更好的方式。它也很容易实现,并且可以无延迟地传递消息。
其关键就在于和服务器之间建立一个挂起的链接。如果连接丢失,可能是因为网络错误,浏览器会立即发送一个新请求。
实现长轮询的客户端 subscribe 函数的示例代码:
async function subscribe() {
let response = await fetch("/subscribe"); // 服务器在需要的时候返回响应
if (response.status == 502) {
// 状态 502 是连接超时错误 让我们重新连接
await subscribe();
} else if (response.status != 200) {
// 一个 error —— 让我们显示它
showMessage(response.statusText);
await new Promise((resolve) => setTimeout(resolve, 1000));
await subscribe(); // 一秒后重新连接
} else {
// 获取到正常数据 —— 让我们显示它
let message = await response.text();
showMessage(message);
await subscribe(); // 再次调用 subscribe() 以获取下一条消息
}
}
subscribe();
# 使用场景
在消息很少的情况下,长轮询很有效。如果消息比较频繁,首选另一种方法,例如:Websocket 或 Server Sent Events。