Как реализовать параллельную загрузку больших файлов в JavaScript?

внешний интерфейс JavaScript

существуетКак реализовать контроль параллелизма в JavaScript?В этой статье брат Абао подробно анализируетasync-poolКак использовать эту библиотекуPromise.allиPromise.raceФункция реализует параллелизм управления асинхронными задачами. В этой статье Baoge расскажет, как использоватьasync-poolпредоставляется этой библиотекойasyncPool функция для параллельной загрузки больших файлов.

Я считаю, что некоторые мелкие партнеры уже поняли решение для загрузки больших файлов.При загрузке больших файлов, чтобы повысить эффективность загрузки, мы обычно используемBlob.sliceМетод обрезает большой файл в соответствии с указанным размером, а затем запускает многопоточную загрузку по частям. После успешной загрузки всех частей он уведомляет сервер о необходимости их объединения.

Итак, можем ли мы принять аналогичную идею для загрузки больших файлов? Поддержка на сервереRangeПри условии заголовка запроса мы также можем реализовать функцию многопоточной загрузки блока, как показано на следующем рисунке:

Прочитав приведенную выше картинку, я полагаю, что вы уже имеете определенное представление о схеме загрузки больших файлов. Далее, давайте сначала представим запросы диапазона HTTP.

Следуйте «Дорога полного стека», чтобы прочитать 4 бесплатные электронные книги (всего более 30 000 загрузок) и 11 руководств по Vue 3 для продвинутых пользователей.

1. Запрос области HTTP

Запросы области протокола HTTP позволяют серверу отправлять клиенту только часть сообщения HTTP. Запросы диапазона полезны при передаче больших медиафайлов или в сочетании с функцией возобновления загрузки файла. если присутствует в ответеAccept-Rangesзаголовок (и его значение не "none"), то сервер поддерживает запросы диапазона.

В заголовке Range можно одновременно запросить несколько частей, и сервер вернет их в виде составного файла. Если сервер возвращает ответ диапазона, вам нужно использовать206 Partial Contentкод состояния. Если запрошенный диапазон недействителен, сервер вернет416 Range Not SatisfiableКод состояния, указывающий на ошибку клиента. Серверу разрешено игнорировать заголовок Range и, таким образом, возвращать весь файл с кодом состояния 200 .

1.1 Синтаксис диапазона

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: единица измерения, используемая для запроса диапазона, обычно байты.
  • <range-start>: целое число, представляющее начальное значение диапазона в указанных единицах измерения.
  • <range-end>: целое число, представляющее конечное значение диапазона в указанных единицах измерения.Это значение является необязательным и, если его нет, указывает, что область действия распространяется до конца документа.

пониматьRangeПосле синтаксиса давайте взглянем на реальный пример использования:

1.1.1 Единая область применения
$ curl http://i.imgur.com/z4d4kWk.jpg -i -H "Range: bytes=0-1023"
1.1.2 Несколько областей применения
$ curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"

Что ж, сначала здесь будут представлены соответствующие знания о HTTP-запросе диапазона, давайте перейдем к теме и начнем знакомить, как реализовать загрузку больших файлов.

2. Как реализовать загрузку больших файлов

Для того, чтобы дать вам лучшее понимание того, что следует, давайте взглянем на общую блок-схему:

Разобравшись с процессом загрузки больших файлов, давайте сначала определим некоторые вспомогательные функции, задействованные в описанном выше процессе.

2.1 Определение вспомогательных функций

2.1.1 Определение функции getContentLength

Как следует из названияgetContentLengthФункция для получения длины файла. В этой функции мы отправляемHEADзапрос, затем прочитайте из заголовков ответаContent-Lengthинформацию для получения текущейurlСоответствует длине содержимого файла.

function getContentLength(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest();
    xhr.open("HEAD", url);
    xhr.send();
    xhr.onload = function () {
      resolve(
        ~~xhr.getResponseHeader("Content-Length") 
      );
    };
    xhr.onerror = reject;
  });
}
2.1.2 Определение функции asyncPool

существуетКак реализовать контроль параллелизма в JavaScript?В этой статье мы знакомимasyncPoolФункция, которая используется для реализации управления параллелизмом для асинхронных задач. Функция принимает 3 параметра:

  • poolLimit(числовой тип): указывает количество одновременных ограничений;
  • array(Тип массива): представляет массив задач;
  • iteratorFn(Тип функции): представляет итеративную функцию, которая используется для обработки каждого элемента задачи и возвращает объект Promise или асинхронную функцию.
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);
}
2.1.3 Определение функции getBinaryContent

getBinaryContentФункция используется для инициирования запроса диапазона на основе входящих параметров, тем самым загружая блоки данных файла в пределах указанного диапазона:

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));
    }
  });
}

должен быть в курсеArrayBufferОбъекты используются для представления общих буферов фиксированной длины необработанных двоичных данных.мы не можем работать напрямуюArrayBuffer, но работать с объектами типизированного массива или объектами DataView, которые представляют данные в буфере в определенном формате, и использовать эти форматы для чтения и записи содержимого буфера..

2.1.4 Определение функции конкатенации

Поскольку он не может управляться напрямуюArrayBufferобъект, поэтому нам нужно сначала поставитьArrayBufferобъект, преобразованный вUint8Arrayобъект, а затем выполните операцию слияния. определено нижеconcatenateФункция состоит в том, чтобы объединить загруженные блоки данных файла.Конкретный код выглядит следующим образом:

function concatenate(arrays) {
  if (!arrays.length) return null;
  let totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
  let result = new Uint8Array(totalLength);
  let length = 0;
  for (let array of arrays) {
    result.set(array, length);
    length += array.length;
  }
  return result;
}
2.1.5 Определение функции saveAs

saveAsФункция используется для реализации функции сохранения файла клиента, вот только простая реализация. В реальных проектах вы можете рассмотреть возможность использованияFileSaver.js . если ты правFileSaver.js Если вам интересно, как это работает, вы можете прочитатьДавайте поговорим о 15.5K FileSaver, как он работает?Эта статья.

function saveAs({ name, buffers, mime = "application/octet-stream" }) {
  const blob = new Blob([buffers], { type: mime });
  const blobUrl = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.download = name || Math.random();
  a.href = blobUrl;
  a.click();
  URL.revokeObjectURL(blob);
}

существуетsaveAsВ функции мы использовали Blob и Object URL. Где URL-адрес объекта — это псевдопротокол, который позволяет использовать объекты Blob и File в качестве источников URL-адресов для изображений, ссылок для загрузки двоичных данных и т. д. В браузере мы используемURL.createObjectURLметод для создания URL-адреса объекта, который принимаетBlobобъекта и создать для него уникальный URL видаblob:<origin>/<uuid>, соответствующий пример выглядит следующим образом:

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

Внутри браузера для каждого проходаURL.createObjectURLСгенерированный URL-адрес сохраняет URL-адрес → Карта BLOB-объектов. Поэтому такие URL короче, но к ним можно получить доступBlob. Сгенерированный URL-адрес действителен только в том случае, если текущий документ открыт. Что ж, сначала здесь будет представлено связанное содержимое URL-адреса объекта.Если вы знаете больше о Blob и URL-адресе объекта, вы можете прочитатьКапли, о которых вы не зналиЭта статья.

2.1.6 Определение функции загрузки

downloadФункция используется для реализации операции загрузки, она поддерживает 3 параметра:

  • url(Строковый тип): адрес предварительно загруженного ресурса;
  • chunkSize(числовой тип): размер блока в байтах;
  • poolLimit(числовой тип): указывает предел параллелизма.
async function download({ url, chunkSize, poolLimit = 1 }) {
  const contentLength = await getContentLength(url);
  const chunks = typeof chunkSize === "number" ? Math.ceil(contentLength / chunkSize) : 1;
  const results = await asyncPool(
    poolLimit,
    [...new Array(chunks).keys()],
    (i) => {
      let start = i * chunkSize;
      let end = i + 1 == chunks ? contentLength - 1 : (i + 1) * chunkSize - 1;
      return getBinaryContent(url, start, end, i);
    }
  );
  const sortedBuffers = results
    .map((item) => new Uint8Array(item.buffer));
  return concatenate(sortedBuffers);
}

2.2 Пример использования загрузки больших файлов

Основываясь на вспомогательных функциях, определенных выше, мы можем легко загружать большие файлы параллельно.Конкретный код выглядит следующим образом:

function multiThreadedDownload() {
  const url = document.querySelector("#fileUrl").value;
  if (!url || !/https?/.test(url)) return;
  console.log("多线程下载开始: " + +new Date());
  download({
    url,
    chunkSize: 0.1 * 1024 * 1024,
    poolLimit: 6,
  }).then((buffers) => {
    console.log("多线程下载结束: " + +new Date());
    saveAs({ buffers, name: "我的压缩包", mime: "application/zip" });
  });
}

Так как полный пример кода содержит много контента, Brother Abao не будет помещать конкретный код. Заинтересованные партнеры могут посетить следующий адрес, чтобы просмотреть пример кода.

Полный пример кода:gist.GitHub.com/Semelinker/8…

Здесь мы смотрим на текущие результаты примера загрузки большого файла:

3. Резюме

В этой статье описывается, как использовать в JavaScriptasync-poolпредоставляется этой библиотекойasyncPool функция для параллельной загрузки больших файлов. Помимо введенияasyncPool В дополнение к функции A Baoge также рассказал, как получить размер файла с помощью запроса HEAD, как инициировать запрос диапазона HTTP и как сохранить файл на стороне клиента. На самом деле использоватьasyncPool Функция может реализовать не только параллельную загрузку больших файлов, но и реализовать параллельную загрузку больших файлов.Заинтересованные партнеры могут попробовать сами.

Следуйте «Дорога полного стека», чтобы прочитать 4 бесплатные электронные книги (всего более 30 000 загрузок) и 11 руководств по Vue 3 для продвинутых пользователей.Друзья, которые хотят вместе изучать TS/Vue 3.0, могут добавить Abaoge WeChat —— semlinker.

4. Справочные ресурсы