如何做好大文件下载

1,695 阅读5分钟

这是我参与8月更文挑战的第11天,活动详情查看:       8月更文挑战 ​

序言

在我们工作中,经常会遇见大文件下载的场景,例如向服务端请求视屏,音频,或者一些压缩包,如果只是简单的下载,那么会花费大量的时间,用户体验极差,那么我们如何做好大文件下载,提升速率,强化用户体验呢。

提速的方法

生活中,有一件复杂的事情,比如要搬100箱水,一个人搬可能要一上午,但是10个人搬,效率就能提高10倍。再比如有100辆车要过一座桥,只开放了一个车道,一次只能过一辆,如果我们想要尽快全部通过,就要多开放一些通道,也就是空间换时间的概念。

在我们的大文件下载中,我们也会应用到这种概念,我们会以这种理念为核心,设计出一种合理的下载方式,来增加我们的下载渠道,提升速率。

实现方法预设

  1. 我们要想办法把服务端的一个大文件分成多个请求。

  2. 如何控制下载流。

  3. 下载完成之后如何拼装。

  4. 最后进行保存

我们来看一个整理好的流程图

image.png

查询服务端是否支持HTTP范围请求

Accept-Ranges

Accept-Ranges 响应的 HTTP 标头是由服务器使用以通告其支持部分请求的标志物。此字段的值表示可用于定义范围的单位。

如果存在 Accept-Range 标题,浏览器可能会尝试恢复中断的下载,而不是从头再次开始

Accept-Range 有两种状态

Accept-Ranges: bytes
Accept-Ranges: none

查询服务端是否支持HTTP范围请求

Accept-Ranges

Accept-Ranges 响应的 HTTP 标头是由服务器使用以通告其支持部分请求的标志物。此字段的值表示可用于定义范围的单位。

如果存在 Accept-Range 标题,浏览器可能会尝试恢复中断的下载,而不是从头再次开始

Accept-Range 有两种状态

Accept-Ranges: bytes
Accept-Ranges: none

查询服务端是否支持HTTP范围请求

Accept-Ranges

Accept-Ranges 响应的 HTTP 标头是由服务器使用以通告其支持部分请求的标志物。此字段的值表示可用于定义范围的单位。

如果存在 Accept-Range 标题,浏览器可能会尝试恢复中断的下载,而不是从头再次开始

Accept-Range 有两种状态

Accept-Ranges: bytes
Accept-Ranges: none

none 没有范围单位被支持,这使得它的标题相当于它自己的缺席,因此很少使用,尽管一些浏览器,如 IE9 ,它被用来禁用或删除下载管理器中的暂停按钮。

bytes 范围的单位是字节。

Range

range的作用是一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。

状态码为 206 Partial Content:服务器返回的是范围响应。

状态码为 416 Range Not Satisfiable : 表示请求范围不合法,客户端错误。

状态码为 200 :服务器允许忽略 Range 头部,返回整个文件

使用方法

Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>

unit : 范围请求所采用的单位,通常是字节(bytes)。

range-start : 范围的起始值,一个整数。

range-end : 范围的结束值,可选,如果不存在,就一直延续到文件末端。

发送范围请求

写一个方法从入参和出参开始

首先我们发送范围请求,首先要有 url ,然后就是 Range 的开始和结束。但是因为发送的是范围请求,最后我们还要考虑到文件的拼接,所以还需要一个文件当前的索引值,最后我们确认出我们的入参是(url,start,end,i)

function getBinaryContent(url, start, end, i) {
  return new Promise((resolve, reject) => {
    try {
      let xhr = new XMLHttpRequest();
      xhr.open("GET", url, true);
      xhr.setRequestHeader("range", `bytes=${start}-${end}`); // 请求头上设置范围请求信息
      xhr.responseType = "arraybuffer"; // 设置返回的类型为arraybuffer
      xhr.onload = function () {
        resolve({
          index: i, // 文件块的索引
          buffer: xhr.response, // 范围请求对应的数据
        });
      };
      xhr.send();
    } catch (err) {
      reject(new Error(err));
    }
  });
}

获取文件大小

通过 HEAD 请求方式

var url = 'http://'; //文件下载链接
var fileSize = 0; //下载文件大小    
var xhr = new XMLHttpRequest();
    xhr.open('HEAD', url, true);    // 也可用POST方式
    xhr.onreadystatechange = () => {
     if (xhr.readyState == 4) {
       if (xhr.status == 200) {
         fileSize = xhr.getResponseHeader('Content-Length');
         console.log(fileSize)
       } else {
         alert('ERROR');
       }
     }
    };
    xhr.send()

我们通过请求头的方式获取到了文件大小以后就可以发送范围请求了,接下来考虑的问题就是设计 asyncPool 并发控制执行我们的范围请求了

并发下载

我们实现并发下载,实际是应用了async-pool 的实现原理。关于这个库有一篇不错的文章,想要深入理解的同学点击这里

我们可以知道,入参为最大任务数量,任务数组

async function asyncPool(poolLimit, array, iteratorFn) {
  const ret = []; // 存储所有的异步任务
  const executing = []; // 存储正在执行的异步任务
  for (const item of array) {
    // 调用iteratorFn函数创建异步任务
    const p = Promise.resolve().then(() => iteratorFn(item, array));
    ret.push(p); // 保存新的异步任务

    // 当poolLimit值小于或等于总任务个数时,进行并发控制
    if (poolLimit <= array.length) {
      // 当任务完成后,从正在执行的任务数组中移除已完成的任务
      const e = p.then(() => executing.splice(executing.indexOf(e), 1));
      executing.push(e); // 保存正在执行的异步任务
      if (executing.length >= poolLimit) {
        await Promise.race(executing); // 等待较快的任务执行完成
      }
    }
  }
  return Promise.all(ret);
}