Fetch: Download progress by Nordtonito · Pull Request #294 · javascript-tutorial/uk.javascript.info

@@ -1,60 +1,60 @@
# Fetch: Download progress # Fetch: Хід завантаження
The `fetch` method allows to track *download* progress. Метод `fetch` дозволяє відстежувати хід *завантаження*.
Please note: there's currently no way for `fetch` to track *upload* progress. For that purpose, please use [XMLHttpRequest](info:xmlhttprequest), we'll cover it later. Будь ласка, зверніть увагу: наразі `fetch` не може відстежувати хід *вивантаження*. Для цієї мети використовуйте [XMLHttpRequest](info:xmlhttprequest), ми розглянемо його пізніше.
To track download progress, we can use `response.body` property. It's a `ReadableStream` -- a special object that provides body chunk-by-chunk, as it comes. Readable streams are described in the [Streams API](https://streams.spec.whatwg.org/#rs-class) specification. Щоб відстежувати хід завантаження, ми можемо використовувати властивість `response.body`. Це `ReadableStream` -- спеціальний об’єкт, який надає тіло відповіді фрагментами, в міру надходження. Потоки для зчитування описані в специфікації [Streams API](https://streams.spec.whatwg.org/#rs-class).
Unlike `response.text()`, `response.json()` and other methods, `response.body` gives full control over the reading process, and we can count how much is consumed at any moment. На відміну від `response.text()`, `response.json()` та інших методів, `response.body` дає повний контроль над процесом зчитування, і ми можемо підрахувати, скільки даних отримано в будь-який момент.
Here's the sketch of code that reads the response from `response.body`: Ось приклад коду, який зчитує відповідь з `response.body`:
```js // instead of response.json() and other methods // замість response.json() та інших методів const reader = response.body.getReader();
// infinite loop while the body is downloading // нескінченний цикл, поки тіло відповіді завантажується while(true) { // done is true for the last chunk // value is Uint8Array of the chunk bytes // done стає true в останньому фрагменті // value -- Uint8Array з байтів кожного фрагмента const {done, value} = await reader.read();
if (done) { break; }
console.log(`Received ${value.length} bytes`) console.log(`Отримано ${value.length} байт`) } ```
The result of `await reader.read()` call is an object with two properties: - **`done`** -- `true` when the reading is complete, otherwise `false`. - **`value`** -- a typed array of bytes: `Uint8Array`. Результатом виклику `await reader.read()` є об’єкт з двома властивостями: - **`done`** -- `true`, коли зчитування завершено, інакше -- `false`. - **`value`** -- типізований масив байтів: `Uint8Array`.
```smart Streams API also describes asynchronous iteration over `ReadableStream` with `for await..of` loop, but it's not yet widely supported (see [browser issues](https://github.com/whatwg/streams/issues/778#issuecomment-461341033)), so we use `while` loop. Streams API також описує асинхронну ітерацію над `ReadableStream` з циклом `for await..of`, але він ще не широко підтримується (дивись [баги браузерів](https://github.com/whatwg/streams/issues/778#issuecomment-461341033)), тому ми використовуємо цикл `while`. ```
We receive response chunks in the loop, until the loading finishes, that is: until `done` becomes `true`. Ми отримуємо фрагменти відповідей у циклі, поки не закінчиться завантаження, тобто доки `done` не стане `true`.
To log the progress, we just need for every received fragment `value` to add its length to the counter. Щоб відстежити прогрес, нам просто потрібно для кожного отриманого `value` фрагмента додати його довжину до лічильника.
Here's the full working example that gets the response and logs the progress in console, more explanations to follow: Ось повний робочий приклад, який отримує відповідь та показує прогрес у консолі, з додатковими поясненнями:
```js run async // Step 1: start the fetch and obtain a reader // Крок 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();
// Step 2: get total length // Крок 2: отримуємо загальну довжину const contentLength = +response.headers.get('Content-Length');
// Step 3: read the data let receivedLength = 0; // received that many bytes at the moment let chunks = []; // array of received binary chunks (comprises the body) // Крок 3: зчитуємо дані let receivedLength = 0; // кількість байтів, отриманих на даних момент let chunks = []; // масив отриманих бінарних фрагментів (що складають тіло відповіді) while(true) { const {done, value} = await reader.read();
Expand All @@ -65,50 +65,50 @@ while(true) { chunks.push(value); receivedLength += value.length;
console.log(`Received ${receivedLength} of ${contentLength}`) console.log(`Отримано ${receivedLength} з ${contentLength}`) }
// Step 4: concatenate chunks into single Uint8Array // Крок 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; }
// Step 5: decode into a string // Крок 5: декодуємо в рядок let result = new TextDecoder("utf-8").decode(chunksAll);
// We're done! // Готово! let commits = JSON.parse(result); alert(commits[0].author.login); ```
Let's explain that step-by-step: Пояснімо це крок за кроком:
1. We perform `fetch` as usual, but instead of calling `response.json()`, we obtain a stream reader `response.body.getReader()`. 1. Ми виконуємо `fetch` як зазвичай, але замість того, щоб викликати `response.json()`, отримуємо доступ до потоку зчитування `response.body.getReader()`.
Please note, we can't use both these methods to read the same response: either use a reader or a response method to get the result. 2. Prior to reading, we can figure out the full response length from the `Content-Length` header. Зауважте, що ми не можемо використовувати обидва ці методи для зчитування однієї відповіді: щоб отримати результат, скористайтеся зчитувачем `response.json()` або методом `response.body()`. 2. Перед зчитуванням ми можемо визначити повну довжину відповіді із заголовка `Content-Length`.
It may be absent for cross-origin requests (see chapter <info:fetch-crossorigin>) and, well, technically a server doesn't have to set it. But usually it's at place. 3. Call `await reader.read()` until it's done. Він може бути відсутнім для запитів між джерелами (дивись розділ <info:fetch-crossorigin>), і, взагалі-то, технічно сервер не зобов’язаний його встановлювати. Але зазвичай він присутній. 3. Викликаємо `await reader.read()`, до закінчення завантаження.
We gather response chunks in the array `chunks`. That's important, because after the response is consumed, we won't be able to "re-read" it using `response.json()` or another way (you can try, there'll be an error). 4. At the end, we have `chunks` -- an array of `Uint8Array` byte chunks. We need to join them into a single result. Unfortunately, there's no single method that concatenates those, so there's some code to do that: 1. We create `chunksAll = new Uint8Array(receivedLength)` -- a same-typed array with the combined length. 2. Then use `.set(chunk, position)` method to copy each `chunk` one after another in it. 5. We have the result in `chunksAll`. It's a byte array though, not a string. Ми збираємо фрагменти відповідей у масиві `chunks`. Це важливо, оскільки після того, як відповідь буде використана, ми не зможемо "перезчитати" її за допомогою `response.json()` або іншим способом (ви можете спробувати -- буде помилка). 4. У кінці ми маємо `chunks` -- масив байтових фрагментів `Uint8Array`. Нам потрібно об’єднати їх в єдиний результат. На жаль, немає єдиного методу, який би їх об’єднав, тому для цього є певний код: 1. Ми створюємо `chunksAll = new Uint8Array(receivedLength)` -- однотипний масив із заданою довжиною. 2. Потім використовуємо метод `.set(chunk, position)`, щоб скопіювати у нього кожен `chunk` один за одним. 5. Маємо результат у `chunksAll`. Але це байтовий масив, а не рядок.
To create a string, we need to interpret these bytes. The built-in [TextDecoder](info:text-decoder) does exactly that. Then we can `JSON.parse` it, if necessary. Щоб створити рядок, нам потрібно інтерпретувати ці байти. Вбудований [TextDecoder](info:text-decoder) робить саме це. Потім ми можемо перетворити рядок на дані за допомогою `JSON.parse`, якщо необхідно.
What if we need binary content instead of a string? That's even simpler. Replace steps 4 and 5 with a single line that creates a `Blob` from all chunks: Що робити, якщо нам потрібен результат у бінарному вигляді замість рядка? Це ще простіше. Замініть кроки 4 і 5 рядком, який створює `Blob` з усіх фрагментів: ```js let blob = new Blob(chunks); ```
At the end we have the result (as a string or a blob, whatever is convenient), and progress-tracking in the process. Наприкінці ми маємо результат (як рядок або `Blob`, як зручно) і відстеження прогресу в процесі.
Once again, please note, that's not for *upload* progress (no way now with `fetch`), only for *download* progress. Ще раз зауважте, що це не для процесу *вивантаження* даних на сервер (зараз немає змоги використовувати `fetch`) -- лише для процесу *завантаження* даних з сервера.
Also, if the size is unknown, we should check `receivedLength` in the loop and break it once it reaches a certain limit. So that the `chunks` won't overflow the memory. Крім того, якщо розмір завантаження невідомий, ми повинні перевірити `receivedLength` у циклі та зупинити його, як тільки воно досягне певної межі. Щоб `chunks` не переповнювали пам’ять.