使用XHR和fetch发送请求,监控请求发送及响应接收进度

零、前言

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

本文讨论的主题是:使用上述两种方式发送http请求时,如何监控请求发送和响应接收的进度(这在传输数据量较大或网络环境较差的情况下会是一个对用户友好的功能)。

一、使用XHR实现

使用XHR发送http请求是现在较多项目的选择,它有一个闻名的二次封装库------axios。但文本并不讨论基于axios实现需求,而是使用原生的XHR方式。

1-1 监控请求发送进度

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

  1. upload对象
  2. progress事件。

upload对象是xhr实例中的属性,它用于表示上传的进程,而同时这个对象可以用来添加事件监听器,监听progress事件,就能获得我们需要的信息。

javascript 复制代码
// 监控请求发送进度
xhr.upload.addEventListener('progress', (e) => {
  // loaded属性代表已经传输的数据量,total代表需要传输的总数据量。
  // 单位是字节
  console.log(e.loaded, e.total)
  console.log( (e.loaded / e.total * 100).toFixed(2) + '%' )
})

progress事件是在数据传输过程中周期性触发,并不是每传输一个数据包触发一次,所以并非100%实时,但这足够向用户反映进度。

1-2 监控响应接收进度

与监控请求发送的进度类似,监听响应接收的进度也是使用事件监听器progress,但是添加的对象就不是xhr.upload对象,而是xhr对象本身。

javascript 复制代码
xhr.addEventListener('progress', (e) => {
  // loaded属性代表已经接收的数据量,total代表需要接收的总数据量。
  // 单位是字节
  console.log(e.loaded, e.total)
  console.log( (e.loaded / e.total * 100).toFixed(2) + '%' )
})

此progress事件也是在数据传输过程中周期性触发

1-3 完整代码

旨在方便理解本文主题,如下函数封装中省略了很多现实中需要处理的情况(如检查http状态码等)。

javascript 复制代码
function request(options = {}) {
  const { url, method = 'GET', data = null } = options

  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    
    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.open(method, url)
    xhr.send(data)
  })
}

二、使用fetch实现

fetch是一个比XHR更加现代的网络请求api,但因为其原生能力的限制,使用fetch发送请求时,只能监控响应接收进度,不能监控请求发送的进度。

2-1 监控响应接收进度

fetch并没有提供像XHR中的loaded和total属性来直接展示现时数据传输量,而是需要手动计算得出结果。

javascript 复制代码
async function request(){
    const response = await fetch(url)
}

上述代码展示了使用fetch发送请求的情况,其返回值response是一个Promise对象,并且它会在响应头接收完之后被解析为resolve(即使服务器返回的http状态码为4xx或5xx)

2-1-1 获取响应体中的总数据量

读取响应头中content-length,并将其转换成Number类型,可以得出服务器将要往浏览器传输的数据量。

javascript 复制代码
async function request(){
    const response = await fetch(url)
    
    // 获取响应头中的content-length,代表响应体的总数据量有多少(单位字节)
    const total = +response.headers.get('content-length');
}

2-1-2 计算目前接收的数据量

因为fetch可以流式读取响应体,所以我们可以实时累加已经接收的数据量

javascript 复制代码
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)
    }
}

需要注意的是,我们response.body是一个可读流,我们使用getReader()来读取它之后,就不可以在后续调用fetch原生的response.json()等api来格式化数据了(因为流式数据只能被消费一次)。

所以我们后续需要手动拼接数据体,并作为request函数的返回值供调用者使用。

javascript 复制代码
async function request(){
    const response = await fetch(url)
    
    // 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
      // 在这里实时获取已经接收的响应体数据
      console.log(loaded)
      
      // 手动拼接响应体数据
      body = body + decoder.decode(value)
    }
    return body
}

2-1-3 完整代码

javascript 复制代码
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
}
相关推荐
卸任6 小时前
Vibe coding工具使用心得
前端·ai编程
brien97 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance7 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M7 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写7 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李7 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易8 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding8 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马8 小时前
前沿视界
前端
kyriewen8 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员