😁深入JS(八): 了解 Xhr 与 Fetch 如何上传与加载进度条

257 阅读4分钟

前言

在现代web项目中,我们一般使用XHR(XMLHttpRequest)或fetch发送http请求,以实现网页在不刷新的情况下获取数据(即实现AJAX)。

本文讨论的主题是:使用上述两种方式发送http请求时,获取文件上传与加载的进度条

一、使用XHR实现获取上传与加载进度条

1.1、获取上传进度

使用xhr监控请求发送进度,有两个需要注意的关键词:

  1. upload对象
  2. 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 的可读流。

这种可读流都有一个特点,就是在同一时间只能被一个人读取,那么你想想,请求里的流是不是被浏览器读取了?浏览器把这个流读出来,然后发送到了服务器,所以说我们就读不了了