W3docs

Javascript Fetch: прогресс загрузки

Как отслеживать прогресс загрузки с помощью Fetch API и ReadableStream в JavaScript: пошаговое руководство с примерами.

Fetch API — это современный способ выполнять сетевые запросы в JavaScript, однако await fetch(...) разрешается сразу после получения заголовков ответа — задолго до завершения загрузки тела. Чтобы отобразить индикатор прогресса при скачивании большого файла, нужно читать тело ответа по частям и подсчитывать байты по мере их поступления. В этой главе рассказывается, как сделать это с помощью ReadableStream, зачем нужен Content-Length, как построить UI прогресса и что fetch не умеет делать (прогресс загрузки на сервер).

Почему для отображения прогресса fetch требует потока

Когда вы пишете const data = await response.json(), браузер буферизирует всё тело ответа внутри и возвращает его только после полного получения — наблюдать за байтами в процессе невозможно. Однако тело Response доступно как ReadableStream через response.body. Читая чанки из этого потока самостоятельно, можно измерять, сколько данных уже пришло, и обновлять UI при каждом чанке.

Схема всегда одна и та же:

  1. Получить reader: const reader = response.body.getReader().
  2. Организовать цикл на reader.read(), который разрешается в { done, value }value является чанком Uint8Array.
  3. Прибавлять value.length к накопленной сумме и сообщать о прогрессе.
  4. Сохранять чанки, а затем собрать их вместе, когда done равно true.

Чтение тела как ReadableStream

Чтобы знать процент, нужно также знать общий размер. Сервер должен отправить заголовок ответа Content-Length; прочитайте его через response.headers.get('Content-Length'). Он часто отсутствует (chunked transfer encoding, gzip-сжатие или сервер, который просто его не добавляет), поэтому пример ниже использует переданный вызывающим кодом размер в качестве запасного варианта.

javascript— editable

Функция fetchWithProgress загружает данные с указанного URL и отслеживает прогресс скачивания. Она читает тело ответа по чанкам через интерфейс ReadableStream и вызывает коллбэк onProgress, передавая полученный размер и общую длину содержимого. Скачанные данные затем восстанавливаются из чанков и возвращаются в виде декодированной строки.

Примечание

Функция автоматически пытается прочитать заголовок Content-Length. Если заголовок отсутствует (что часто происходит при chunked-передаче или сжатии), она использует параметр fallbackSize. В продакшене разработчик или сервер должны предоставить это запасное значение. Для больших файлов избегайте буферизации всех чанков в памяти; обрабатывайте их инкрементально или используйте response.blob() для не-текстовых ответов.

После завершения цикла у вас будет массив чанков Uint8Array. Чтобы превратить их в пригодные данные, скопируйте их в единый буфер и декодируйте (для текста) или оберните в Blob (для бинарных файлов, изображений, загрузок). Смотрите JavaScript Blob для работы с бинарными данными и запуска скачивания файлов.

Отображение прогресса для пользователей

Индикатор прогресса — это просто элемент, ширина которого отражает received / total. Передайте коллбэк updateProgressBar вместо функции с логированием:

<body>
  <div id="progress-bar" style="width: 100%; background-color: #e0e0e0;">
    <div id="progress" style="width: 0; height: 20px; background-color: #76c7c0;"></div>
  </div>
  <div id="output"></div>
  <script>
    function updateProgressBar(received, total) {
      const progressElement = document.getElementById('progress');
      const percentage = Math.min(100, (received / total) * 100);
      progressElement.style.width = percentage + '%';
    }
    document.addEventListener('DOMContentLoaded', () => {
      const url = 'https://api.w3docs.com/uploads/media/default/0001/05/dd10c28a7052fb6d2ff13bc403842b797a73ff3b.txt';
      const size = 3_900_000; // fallback size
      // fetchWithProgress is defined in the previous code block
      fetchWithProgress(url, updateProgressBar, size)
      .then(data => {
        document.getElementById('output').textContent = 'File content: ' + data.slice(0, 1000) + '...';
      })
      .catch(err => console.error("Download failed:", err));
    });
  </script>
</body>
Внимание

Если вы скачиваете очень большой файл, избегайте слишком частого обновления UI. Например, вместо обновления индикатора прогресса для каждого отдельного чанка можно делать это реже (например, каждые несколько чанков или по временному интервалу). Это помогает поддерживать UI лёгким.

HTML-элементы в приведённом выше примере формируют индикатор прогресса для визуализации хода загрузки и область предформатированного текста для отображения скачанного содержимого. div с id progress-bar служит контейнером, а div с id progress отображает фактический прогресс загрузки.

JavaScript-код обновляет индикатор прогресса на основе полученного объёма данных и общей длины содержимого. Функция updateProgressBar вычисляет процент загруженных данных и соответствующим образом меняет ширину индикатора. Обработчик события запускает функцию fetchWithProgress при загрузке страницы, обновляя индикатор прогресса и отображая скачанное содержимое в элементе output.

Примечание: fetchWithProgress определена в предыдущем фрагменте. В реальном проекте убедитесь, что она находится в области видимости (например, через импорты модулей, сборщик или глобальный тег script).

Что fetch не умеет: прогресс загрузки на сервер

Потоковая техника отслеживает только прогресс скачивания (ответа). На сегодняшний день не существует стандартного способа наблюдать за прогрессом загрузки на сервер (тела запроса) с помощью fetch — тело запроса не доступно как наблюдаемый поток в браузерах. Если вам нужен индикатор прогресса при отправке файла, используйте XMLHttpRequest, чей объект upload генерирует события progress:

function uploadWithProgress(url, file, onProgress) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', url);

    // upload.onprogress fires repeatedly while the body is sent
    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const percent = Math.round((event.loaded / event.total) * 100);
        onProgress(event.loaded, event.total, percent);
      }
    };

    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(new Error('Upload failed'));
    xhr.send(file);
  });
}

event.lengthComputable сообщает, известно ли значение event.total; всегда проверяйте это условие перед вычислением процента.

Отмена выполняющейся загрузки

Длительную загрузку нужно иметь возможность отменить. Передайте AbortSignal в fetch и вызовите controller.abort(), чтобы остановить как запрос, так и цикл потока — полный паттерн описан в разделе Fetch: Abort.

Профессиональные советы

  • Работа с бинарными данными: TextDecoder работает только с текстом. Используйте response.blob() или response.arrayBuffer() для бинарных файлов.
  • Ограничение частоты обновления прогресса: частое чтение чанков может блокировать UI-поток. Применяйте throttle или debounce к коллбэку прогресса.
  • Управление памятью: избегайте хранения всех чанков в массиве для больших файлов. Обрабатывайте их инкрементально.
  • Запасное значение Content-Length: заголовок часто отсутствует. Всегда предоставляйте надёжный fallbackSize.
  • Оптимизация обратной связи с пользователем: предоставление обратной связи в реальном времени о прогрессе загрузки повышает удовлетворённость пользователей и делает приложение более отзывчивым.
  • Использование возможностей браузера: разные браузеры могут по-разному поддерживать расширенные функции. Тестируйте реализацию в нескольких браузерах для обеспечения совместимости.
  • Упрощение при возможности: если потоковое отслеживание прогресса не является обязательным, response.arrayBuffer() предлагает более простой способ восстановления данных без ручного склеивания чанков.

Вооружившись этими знаниями и примерами, вы готовы реализовать Fetch API с отслеживанием прогресса загрузки в своих проектах, обеспечивая пользователям плавный и информативный опыт.

Заключение

Освоение Fetch API предполагает понимание не только того, как выполнять базовые запросы, но и как решать более сложные задачи, такие как отслеживание прогресса загрузки. Используя ReadableStream, мы можем отслеживать загрузки и предоставлять о них обратную связь, значительно улучшая пользовательский опыт. Применение этих техник сделает ваши приложения надёжными, удобными для пользователей и способными эффективно обрабатывать большие объёмы передаваемых данных.

Практика

Практика
Каковы ключевые шаги для отслеживания прогресса загрузки с помощью Fetch API?
Каковы ключевые шаги для отслеживания прогресса загрузки с помощью Fetch API?
Was this page helpful?