前言
在现代web项目中,我们一般使用XHR(XMLHttpRequest)或fetch发送http请求,以实现网页在不刷新的情况下获取数据(即实现AJAX)。
本文讨论的主题是:使用上述两种方式发送http请求时,获取文件上传与加载的进度条
一、使用XHR实现获取上传与加载进度条
1.1、获取上传进度
使用xhr监控请求发送进度,有两个需要注意的关键词:
upload对象progress事件。
upload对象是xhr实例中的属性,它用于表示上传的进程- 同时这个对象可以用来添加事件监听器,监听
progress事件 progress事件中存在两个属性e.loaded(已经传输的数据量)和e.total(传输的总数据量)
<input type="file" onchange="upload(this.files[0])">
<script>
function upload(file) {
let xhr = new XMLHttpRequest();
// 跟踪上传进度
xhr.upload.onprogress = function(e) {
// 单位是字节
console.log( (e.loaded / e.total * 100).toFixed(2) + '%' )
};
xhr.open("POST", "/article/xmlhttprequest/post/upload");
xhr.send(file);
}
</script>
progress事件是在数据传输过程中周期性触发,并不是每传输一个数据包触发一次,所以并非100%实时,但这足够向用户反映进度。
1.2、获取加载进度
与监控请求发送的进度类似,监听响应接收的进度也是使用事件监听器
progress但是添加的对象就不是
xhr.upload对象,而是xhr对象本身。
xhr.addEventListener('progress', (e) => {
// loaded属性代表已经接收的数据量,total代表需要接收的总数据量。
// 单位是字节
console.log(e.loaded, e.total)
console.log( (e.loaded / e.total * 100).toFixed(2) + '%' )
})
1.3、上传进度与加载进度的完整案例
function request(options = {}) {
const { url, method = 'GET', data = null } = options
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(method, url)
xhr.addEventListener('readystatechange', () => {
// 数据完成传输,请求已经完成(无论成功或是失败)
if (xhr.readyState === 4) {
resolve(xhr.responseText)
}
})
// 监控请求发送进度
xhr.upload.addEventListener('progress', e => {
// 单位是字节
console.log(e.loaded, e.total)
console.log( (e.loaded / e.total * 100).toFixed(2) + '%' )
})
// 进度事件,当服务器数据发送过来时,每过来一部分,就触发一次此事件
xhr.addEventListener('progress', (e) => {
// 单位是字节
console.log(e.loaded, e.total)
console.log( (e.loaded / e.total * 100).toFixed(2) + '%' )
})
xhr.send(data)
})
}
二、使用fetch实现获取加载加载进度条
简单使用fetch
export function request(options = {}) {
const { url, method = "GET", data = null } = options;
return new Promise(async (resolve) => {
const resp = await fetch(url, {
method,
body: data,
});
const body = await resp.json();
resolve(body);
});
}
2.1、获取总数据量
读取响应头中content-length,并将其转换成Number类型
async function request(){
const response = await fetch(url)
// 获取响应头中的content-length,代表响应体的总数据量有多少(单位字节)
const total = +response.headers.get('content-length');
}
2.2、获取已经传输的数据量
2.2.1、fetch是一个数据流
因为fetch可以流式读取响应体,所以我们可以实时累加已经接收的数据量
async function request(){
const response = await fetch(url)
// response.body是一个可读流,调用其读取器getReader。
const reader = response.body.getReader()
// 目前已经接收的数据量(单位字节)
let loaded = 0
while(true){
// 这里读的不是整个响应体,而是目前来的这一部分。done表示响应体是否已经传输完毕
const { done, value } = await reader.read()
if(done) {
break;
}
// 将每次接收的数据量(单位字节)拼接起来
loaded = loaded + value.length
// 在这里实时获取已经接收的响应体数据
console.log(loaded)
}
}
2.2.2、完整案例
fetch中获取的数据为二进制,需要将其转换为文本
(new TextDecoder())
async function request(){
const response = await fetch(url)
// 获取响应头中的content-length,代表响应体的总数据量有多少(单位字节)
const total = +response.headers.get('content-length');
// response.body是一个可读流,调用其读取器getReader。
const reader = response.body.getReader()
// 目前已经接收的数据量(单位字节)
let loaded = 0
// 因为上述response.body.getReader()已经消费了数据流,
// 所以要手动拼接响应体数据,而不能调用response.json()等api。
const decoder = new TextDecoder(); // 当返回的二数据是字符串时,使用TextDecoder将其转换为字符串
let body = ''
while(true){
// 这里读的不是整个响应体,而是目前来的这一部分。done表示响应体是否已经传输完毕
const { done, value } = await reader.read()
if(done) {
break;
}
// 将每次接收的数据量(单位字节)拼接起来
loaded = loaded + value.length
// 手动拼接响应体数据
body = body + decoder.decode(value)
// 实时输出接收进度
console.log( (loaded / total * 100).toFixed(2) + '%')
}
return body
}
2.3、Fetch不能实现上传进度条
有人可能会问了为什么fetch不能实现上传进度条呢?
我们也可以去读body,一部分一部分读,不也能获取上传进度条吗?
我们知道 body 属性的类型都是一个叫做 ReadableStream 的可读流。
这种可读流都有一个特点,就是在同一时间只能被一个人读取,那么你想想,请求里的流是不是被浏览器读取了?浏览器把这个流读出来,然后发送到了服务器,所以说我们就读不了了