4 березня 2024 р.

Fetch: Хід завантаження

Метод fetch дозволяє відстежувати хід завантаження.

Будь ласка, зверніть увагу: наразі fetch не може відстежувати хід вивантаження. Для цієї мети використовуйте XMLHttpRequest, ми розглянемо його пізніше.

Щоб відстежувати хід завантаження, ми можемо використовувати властивість response.body. Це ReadableStream – спеціальний об’єкт, який надає тіло відповіді фрагментами, в міру надходження. Потоки для зчитування описані в специфікації Streams API.

На відміну від response.text(), response.json() та інших методів, response.body дає повний контроль над процесом зчитування, і ми можемо підрахувати, скільки даних отримано в будь-який момент.

Ось приклад коду, який зчитує відповідь з response.body:

// замість response.json() та інших методів
const reader = response.body.getReader();

// нескінченний цикл, поки тіло відповіді завантажується
while(true) {
  // done стає true в останньому фрагменті
  // value -- Uint8Array з байтів кожного фрагмента
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

  console.log(`Отримано ${value.length} байт`)
}

Результатом виклику await reader.read() є об’єкт з двома властивостями:

  • donetrue, коли зчитування завершено, інакше – false.
  • value – типізований масив байтів: Uint8Array.
Будь ласка, зверніть увагу:

Streams API також описує асинхронну ітерацію над ReadableStream з циклом for await..of, але він ще не широко підтримується (дивись баги браузерів), тому ми використовуємо цикл while.

Ми отримуємо фрагменти відповідей у циклі, поки не закінчиться завантаження, тобто доки done не стане true.

Щоб відстежити прогрес, нам просто потрібно для кожного отриманого value фрагмента додати його довжину до лічильника.

Ось повний робочий приклад, який отримує відповідь та показує прогрес у консолі, з додатковими поясненнями:

// Крок 1: починаємо завантаження fetch, отримуємо потік для зчитування
let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits?per_page=100');

const reader = response.body.getReader();

// Крок 2: отримуємо загальну довжину
const contentLength = +response.headers.get('Content-Length');

// Крок 3: зчитуємо дані
let receivedLength = 0; // кількість байтів, отриманих на даних момент
let chunks = []; // масив отриманих бінарних фрагментів (що складають тіло відповіді)
while(true) {
  const {done, value} = await reader.read();

  if (done) {
    break;
  }

  chunks.push(value);
  receivedLength += value.length;

  console.log(`Отримано ${receivedLength} з ${contentLength}`)
}

// Крок 4: об’єднуємо фрагменти в один Uint8Array
let chunksAll = new Uint8Array(receivedLength); // (4.1)
let position = 0;
for(let chunk of chunks) {
  chunksAll.set(chunk, position); // (4.2)
  position += chunk.length;
}

// Крок 5: декодуємо в рядок
let result = new TextDecoder("utf-8").decode(chunksAll);

// Готово!
let commits = JSON.parse(result);
alert(commits[0].author.login);

Пояснімо це крок за кроком:

  1. Ми виконуємо fetch як зазвичай, але замість того, щоб викликати response.json(), отримуємо доступ до потоку зчитування response.body.getReader().

    Зауважте, що ми не можемо використовувати обидва ці методи для зчитування однієї відповіді: щоб отримати результат, скористайтеся зчитувачем response.json() або методом response.body().

  2. Перед зчитуванням ми можемо визначити повну довжину відповіді із заголовка Content-Length.

    Він може бути відсутнім для запитів між джерелами (дивись розділ Fetch: Запити між різними джерелами), і, взагалі-то, технічно сервер не зобов’язаний його встановлювати. Але зазвичай він присутній.

  3. Викликаємо await reader.read(), до закінчення завантаження.

    Ми збираємо фрагменти відповідей у масиві chunks. Це важливо, оскільки після того, як відповідь буде використана, ми не зможемо “перезчитати” її за допомогою response.json() або іншим способом (ви можете спробувати – буде помилка).

  4. У кінці ми маємо chunks – масив байтових фрагментів Uint8Array. Нам потрібно об’єднати їх в єдиний результат. На жаль, немає єдиного методу, який би їх об’єднав, тому для цього є певний код:

    1. Ми створюємо chunksAll = new Uint8Array(receivedLength) – однотипний масив із заданою довжиною.
    2. Потім використовуємо метод .set(chunk, position), щоб скопіювати у нього кожен chunk один за одним.
  5. Маємо результат у chunksAll. Але це байтовий масив, а не рядок.

    Щоб створити рядок, нам потрібно інтерпретувати ці байти. Вбудований TextDecoder робить саме це. Потім ми можемо перетворити рядок на дані за допомогою JSON.parse, якщо необхідно.

    Що робити, якщо нам потрібен результат у бінарному вигляді замість рядка? Це ще простіше. Замініть кроки 4 і 5 рядком, який створює Blob з усіх фрагментів:

    let blob = new Blob(chunks);

Наприкінці ми маємо результат (як рядок або Blob, як зручно) і відстеження прогресу в процесі.

Ще раз зауважте, що це не для процесу вивантаження даних на сервер (зараз немає змоги використовувати fetch) – лише для процесу завантаження даних з сервера.

Крім того, якщо розмір завантаження невідомий, ми повинні перевірити receivedLength у циклі та зупинити його, як тільки воно досягне певної межі. Щоб chunks не переповнювали пам’ять.

Навчальна карта