一、概述
项目中,有时需要在客户端进行大文件的下载,如果在下载的过程中只是一味的让用户进行等待,体验很不好。
所以,一般都会在下载的过程中显示一个下载的进度条,以便让用户知道下载的进度,还有多久能下载完成,不至于让用户在加载的过程中失去信心。
进度条的计算公式也很简单,如下所示。
进度条计算公式
// progress:进度百分比
// downloadedSize:已经下载成功的大小
// totalSize:文件总大小
let progress = parseInt(downloadedSize / totalSize * 100);
原本是一个不算难的需求,但是在获取进度的过程中,却出现了问题,获取的totalSize
一直为0,只有等文件下载完成之后,才能获取totalSize
。
二、解决思路
文件请求使用的是5+ API
中的Downloader
模块中的方法。代码如下。
// 创建下载任务
const downloadTask = plus.downloader.createDownload('文件地址', {}, (download, status) => {
if(status === 200){
console.log(`file download success:${download.filename}`);
}else {
console.error(`file download fail, status code: ${status}`);
}
})
// 监听下载状态改变事件
downloadTask.addEventListener('statechanged'. (download, status) => {
// 状态为3表示正在下载任务接收数据(下载过程中会多次调用)
if (download.state == 3) {
console.log("download.downloadedSize: ", download.downloadedSize);
console.log("download.totalSize: ", download.totalSize);
// 进度计算
let progress = parseInt(download.downloadedSize / download.totalSize * 100);
}
})
上述程序代码中,理论上是可以正常获取进度的,然后实际执行结果却不如人意。
不知为何,downloadedSize
可以正常获取到值。但是,totalSize
的值却一直为0。
在查阅了官方文档之后,发现其中有这么一句话,具体可以看链接。
这里说:totalSize
的值时从HTTP请求响应头数据里面的Content-Length
中获取,如果没有这个值,则totalSize
的值便会一直为0。
但笔者通过抓包的形式,查看了一下这个文件的请求响应数据。发现在响应的头数据是包含了Content-Length
数据的,明显和官网说的不符。
在经过一番查阅之后,笔者终于发现这个问题其实跟HTTP协议中的压缩编码传输有关。其中涉及到了两个请求头,accept-encoding
和content-encoding
。
accept-encoding
是request headers中的一个数据,是用来发送请求时告知服务器,本地的客户端支持什么格式的压缩编码。
content-encoding
response headers中的一个数据,用来在数据响应时由服务器告知客户端,本次的响应数据中,使用了什么格式的压缩编码,如果未使用任何编码,则为空,如果指定了使用编码,则本地客户端要根据编码格式对数据进行解压缩。
问题的关键就出在了这里,如果请求中的数据使用了压缩编码,就无法获取精确的进度运算。
再次通过抓包,发现文件请求中accept-encoding
确实包含了gzip
,服务器响应中的content-encoding
的值也包含了gzip
。证实请求的数据确实进行了gzip
编码压缩。
原本笔者认为这是5+ API
中的缺陷,然而经过笔者的求证之后,发现使用js原生的XMLHttpRequest
对象,也会出现这种问题。只有Firefox浏览器是例外,应该是Firefox本身对此问题做了兼容。
三、解决方案
在通过上述过程知道其中的原理之后,我们便有了解决的方向。
既然是请求的数据经过了压缩编码之后导致无法获取进度信息,那我们就设置需要获取进度的请求不要进行编码压缩。
修改后的代码
// 创建下载任务
const downloadTask = plus.downloader.createDownload('文件地址', {}, (download, status) => {
if(status === 200){
console.log(`file download success:${download.filename}`);
}else {
console.error(`file download fail, status code: ${status}`);
}
})
// 设置请求的请求头accept-encoding信息为空,即告诉服务器不要进行编码压缩
// 此处的空字符串一直要加一个空格,不然在IOS能有效覆盖重写请求头信息,在Android中,会无效。
// downloadTask.setRequestHeader('accept-encoding', '') // 错误写法
downloadTask.setRequestHeader('accept-encoding', ' ') // 正确写法
// 监听下载状态改变事件
downloadTask.addEventListener('statechanged'. (download, status) => {
// 状态为3表示正在下载任务接收数据(下载过程中会多次调用)
if (download.state == 3) {
console.log("download.downloadedSize: ", download.downloadedSize);
console.log("download.totalSize: ", download.totalSize);
// 进度计算
let progress = parseInt(download.downloadedSize / download.totalSize * 100);
}
})
上述解决方式,其实也是有缺陷的,gzip
编码压缩的压缩比其实挺大的。大概有25%左右。一个8M的文件,可以压缩到6M上下。
过也有另外一种解决方式,就是在响应中自定义请求头,返回文件的总大小,然后通过getResponseHeader
方法获取文件的总大小。因为在上述打印结果中,只有totalSize
是获取不到的,downloadedSize
是可以正常获取的。
从理论上讲,笔者认为是可行的。不过笔者觉得有点麻烦,就没有进行实际验证。感兴趣的读者可以尝试一下。