前端获取下载进度——从入门到放弃
前端获取下载进度,从入门到放弃,讲讲如何使用 fetch/xhr 获取下载进度,有哪些弊端。
背景
前端大文件的下载,友好的交互方式是能够显示一个进度条,获取到当前下载了多少,还剩余多少。目前有两种原生的方式获取下载进度,分别是 XMLHttpRequest
的 progress
事件 和 fetch
的 response.body
。
XMLHttpRequest
的方式
XMLHttpRequest 是一个比较旧的 API 了,可以通过监听 XMLHttpRequest 的 progress 事件,来获取下载进度,示例代码如下:
1 | const xhr = new XMLHttpRequest() |
正常情况下,这段代码是可以跑通的。但是显然大多数场景都是不正常的情况,会在控制台输出 cant get progress
。这是为什么?
progress
的 event
实例有以下三个属性需要关注
lengthComputable
: Boolean 值,指出下载进度能否被计算loaded
: 已下载的大小,单位为B
total
: 文件总大小,单位为B,大小和respone.headers
中的Content-Length
一致,实际测试发现,当lengthComputable
为false
时,total
为0
现网会走到 lengthComputable
为 false
的场景,我遇到的一个原因是 gzip
,现网请求时,文件不再以原大小的方式直接返回,而是通过 gzip
之后再返回。
这样就 total
也就是 response.headers
中的 Content-Length
不再是实际文件的大小,而是gzip之后的, 而 loaded
属性是文件已经下载的 gzip 解压之后的实际大小,并不是已经下载的gzip内容的大小,所以从JS层面无法再正确获取到下载的实际进度,所以 lengthComputable
为 false
也就可以解释了。
fetch
的方式
fetch
是一个比较新的API,从发请求的角度来说,fetch
相比于 XMLHttpRequest
更方便调用。在 fetch
刚推出的时候,普遍认为的一个劣势是 fetch
没有办法获取到下载进度,其实借鉴 XMLHttpRequest
的方式,fetch
也能实时获取到下载进度。
fetch
把请求分为了两步,第一步是从发起请求到接收返回头,第二步是 body
内容,所以在 fetch
调用时,如果要获取返回,一般有两个 await
如下:
1 | const response = await fetch('xxxx') |
平时用的比较多的应该是 response.json()
或 response.arrayBuffer()
这两个方法,实际上除此之外, response
还有一个 body
的属性,这个 body
是一个 ReadableStream
实例,一说 Stream
大家应该都懂了,流式读取数据,可以通过 response.body
实时获取后台返回的数据,代码如下:
1 | const downloadWithProgress = async (url, onUpdate) => { |
猛一看,可能会有点疑问,既然已经拿到了 total
值,为什么不一开始创建一个 Uint8Array
,逐次往里面 set,而要全部返回后再实例化 Uint8Array
?
其实和 XMLHttpRequest
是同样的道理,total 是通过 response.headers
中的 Content-Length
获取的,当使用了 gzip
之后,这个 total
值就不准了,而在每一次拿到的 value
值,是 gzip
解压之后的内容,所以 total
和 value
不配套的情况下,无法在起始阶段就分配缓冲区大小,也无法获取到实际的下载进度。
总结
事情到了这里,不管是用 XMLHttpRequest
, 还是使用 fetch
也好,最终都回到了同一个问题上,gzip
之后,无法获取下载进度,除非每次请求都不使用 gzip
之后的,但是这样无异于饮鸩止渴,无论是使用方,还是提供方都需要付出巨大的带宽成本。
此时就需要业务存储文件的实际大小,配合 fetch
或 XMLHttpRequest
才能处理下载进度了。
而且之前没细想,其实也不难发现, gzip
具有加下载加解压的能力。