Метод 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()
є об’єкт з двома властивостями:
done
–true
, коли зчитування завершено, інакше –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);
Пояснімо це крок за кроком:
-
Ми виконуємо
fetch
як зазвичай, але замість того, щоб викликатиresponse.json()
, отримуємо доступ до потоку зчитуванняresponse.body.getReader()
.Зауважте, що ми не можемо використовувати обидва ці методи для зчитування однієї відповіді: щоб отримати результат, скористайтеся зчитувачем
response.json()
або методомresponse.body()
. -
Перед зчитуванням ми можемо визначити повну довжину відповіді із заголовка
Content-Length
.Він може бути відсутнім для запитів між джерелами (дивись розділ Fetch: Запити між різними джерелами), і, взагалі-то, технічно сервер не зобов’язаний його встановлювати. Але зазвичай він присутній.
-
Викликаємо
await reader.read()
, до закінчення завантаження.Ми збираємо фрагменти відповідей у масиві
chunks
. Це важливо, оскільки після того, як відповідь буде використана, ми не зможемо “перезчитати” її за допомогоюresponse.json()
або іншим способом (ви можете спробувати – буде помилка). -
У кінці ми маємо
chunks
– масив байтових фрагментівUint8Array
. Нам потрібно об’єднати їх в єдиний результат. На жаль, немає єдиного методу, який би їх об’єднав, тому для цього є певний код:- Ми створюємо
chunksAll = new Uint8Array(receivedLength)
– однотипний масив із заданою довжиною. - Потім використовуємо метод
.set(chunk, position)
, щоб скопіювати у нього коженchunk
один за одним.
- Ми створюємо
-
Маємо результат у
chunksAll
. Але це байтовий масив, а не рядок.Щоб створити рядок, нам потрібно інтерпретувати ці байти. Вбудований TextDecoder робить саме це. Потім ми можемо перетворити рядок на дані за допомогою
JSON.parse
, якщо необхідно.Що робити, якщо нам потрібен результат у бінарному вигляді замість рядка? Це ще простіше. Замініть кроки 4 і 5 рядком, який створює
Blob
з усіх фрагментів:let blob = new Blob(chunks);
Наприкінці ми маємо результат (як рядок або Blob
, як зручно) і відстеження прогресу в процесі.
Ще раз зауважте, що це не для процесу вивантаження даних на сервер (зараз немає змоги використовувати fetch
) – лише для процесу завантаження даних з сервера.
Крім того, якщо розмір завантаження невідомий, ми повинні перевірити receivedLength
у циклі та зупинити його, як тільки воно досягне певної межі. Щоб chunks
не переповнювали пам’ять.