前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端获取下载进度——从入门到放弃

前端获取下载进度——从入门到放弃

原创
作者头像
扰乱
发布2023-10-31 14:59:37
1.3K0
发布2023-10-31 14:59:37
举报
文章被收录于专栏:前端样式

前端获取下载进度,从入门到放弃,讲讲如何使用 fetch/xhr 获取下载进度,有哪些弊端,业务正确的处理方式是什么。

背景

前端大文件的下载,友好的交互方式是能够显示一个进度条,获取到当前下载了多少,还剩余多少。目前有两种原生的方式获取下载进度,分别是 XMLHttpRequestprogress 事件 和 fetchresponse.body

XMLHttpRequest 的方式

XMLHttpRequest 是一个比较旧的 API 了,可以通过监听 XMLHttpRequest 的 progress 事件,来获取下载进度,示例代码如下:

代码语言:javascript
复制
const xhr = new XMLHttpRequest()
xhr.addEventListener('progress', (event) => {
  if (event.lengthComputable) {
    console.log(event.loaded, event.total)
  } else {
    console.log('cant get progress', event)
  }
})

xhr.open('GET', './data.txt')
xhr.send()

正常情况下,这段代码是可以跑通的。但是显然大多数场景都是不正常的情况,会在控制台输出 cant get progress。这是为什么?

progressevent实例有以下三个属性需要关注

  1. lengthComputable: Boolean 值,指出下载进度能否被计算
  2. loaded: 已下载的大小,单位为 B
  3. total: 文件总大小,单位为B,大小和 respone.headers 中的 Content-Length 一致,实际测试发现,当 lengthComputablefalse 时,total 为0

现网会走到 lengthComputablefalse 的场景,我遇到的一个原因是 gzip,现网请求时,文件不再以原大小的方式直接返回,而是通过 gzip 之后再返回。

这样就 total 也就是 response.headers 中的 Content-Length不再是实际文件的大小,而是gzip之后的, 而 loaded 属性是文件已经下载的 gzip 解压之后的实际大小,并不是已经下载的gzip内容的大小,所以从JS层面无法再正确获取到下载的实际进度,所以 lengthComputablefalse 也就可以解释了。

fetch 的方式

fetch 是一个比较新的API,从发请求的角度来说,fetch 相比于 XMLHttpRequest 更方便调用。在 fetch 刚推出的时候,普遍认为的一个劣势是 fetch 没有办法获取到下载进度,其实借鉴 XMLHttpRequest 的方式,fetch 也能实时获取到下载进度。

fetch 把请求分为了两步,第一步是从发起请求到接收返回头,第二步是 body 内容,所以在 fetch 调用时,如果要获取返回,一般有两个 await 如下:

代码语言:txt
复制
const response = await fetch('xxxx')
const body = await response.json()

平时用的比较多的应该是 response.json()response.arrayBuffer() 这两个方法,实际上除此之外, response 还有一个 body 的属性,这个 body 是一个ReadableStream 实例,一说 Stream 大家应该都懂了,流式读取数据,可以通过 response.body 实时获取后台返回的数据,代码如下:

代码语言:javascript
复制
const downloadWithProgress = async (url, onUpdate) => {
  const response = await fetch(url)
  const total = +response.headers.get('Content-Length')
  const result = []
  let progress = 0
  const reader = response.body.getReader()
  while(true) {
    const { done, value } = await reader.read()
    if (done) {
      break;
    }
    result.push(value)
    progress += value.length
    onUpdate && onUpdate(progress, total)
  }

  let data = new Uint8Array(progress)

  let position = 0
  result.forEach(item => {
    data.set(item, position)
    position += item.length
  })

  return data
} 

猛一看,可能会有点疑问,既然已经拿到了 total 值,为什么不一开始创建一个 Uint8Array,逐次往里面 set,而要全部返回后再实例化 Uint8Array ?

其实和 XMLHttpRequest 是同样的道理,total 是通过 response.headers 中的 Content-Length 获取的,当使用了 gzip 之后,这个 total 值就不准了,而在每一次拿到的 value 值,是 gzip 解压之后的内容,所以 totalvalue 不配套的情况下,无法在起始阶段就分配缓冲区大小,也无法获取到实际的下载进度。

解决方案

事情到了这里,不管是用 XMLHttpRequest, 还是使用 fetch 也好,最终都回到了同一个问题上,gzip 之后,无法获取下载进度,除非每次请求都不使用 gzip 之后的,但是这样无异于饮鸩止渴,无论是服务端,还是客户端都需要付出巨大的带宽成本,有些因小失大了。

那业务应该如何来处理下载进度呢?有两种方式,一是把文件的大小存放在数据库中,在下载的前先获取文件的大小,然后结合已下载的文件大小,就能够正常的获取到下载进度了,缺点是需要维护一份文件的大到到业务存储中。第二种方式是server端实时去获取文件的大小,也是在下载前先获取文件的大小,不同的是文件的大小是通过os提供的能力实时去获取的,这样做的缺点是,如果是热点资源,一直去读取磁盘,效率会很低,而且业务上,一般也不会把可下载的数据和业务逻辑放在一个server上,成本会比较高。当然,也并不是说 xhr 和 fetch 就完全不能用来获取下载进度了,只要能保证 response headers 中的 content-length 和文件大小是一致的,就也可以用xhr/fetch这种方式来获取下载进度。

总结

本文提供了三种获取下载进度的方法,各有优劣,具体业务上使用哪种方式来获取下载进度,还是要结合具体的业务来选择。

P.S. 而且之前没细想,其实从这其中也不难发现, gzip 具有边下载边解压的能力。

我正在参与2023腾讯技术创作特训营第三期有奖征文,组队打卡瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • XMLHttpRequest 的方式
  • fetch 的方式
  • 解决方案
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档