Skip to content

Fetch: Download progress #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 22, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 37 additions & 37 deletions 5-network/03-fetch-progress/article.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@

# Fetch: Download progress

The `fetch` method allows to track *download* progress.
La méthode `fetch` permet de suivre la progression du *téléchargement*.

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.
Veuillez noter: il n'y a actuellement aucun moyen pour `fetch` de suivre la progression du *téléchargement*. À cette fin, veuillez utiliser [XMLHttpRequest](info:xmlhttprequest), nous le couvrirons plus tard.

To track download progress, we can use `response.body` property. It's `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.
Pour suivre la progression du téléchargement, nous pouvons utiliser la propriété `response.body`. C'est `ReadableStream` - un objet spécial qui fournit le corps morceau par morceau, comme il vient. Les flux lisibles sont décrits dans la spécification [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.
Contrairement à `response.text()`, `response.json()` et à d'autres méthodes, `response.body` donne un contrôle total sur le processus de lecture, et nous pouvons compter la quantité consommée à tout moment.

Here's the sketch of code that reads the reponse from `response.body`:
Voici l'esquisse de code qui lit la réponse de `response.body` :

```js
// instead of response.json() and other methods
// au lieu de response.json() et d'autres méthodes
const reader = response.body.getReader();

// infinite loop while the body is downloading
// boucle infinie pendant le téléchargement du corps
while(true) {
// done is true for the last chunk
// value is Uint8Array of the chunk bytes
Expand All @@ -29,32 +29,32 @@ while(true) {
}
```

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`.
Le résultat de l'appel `await reader.read()` est un objet avec deux propriétés :
- **`done`** -- `true` lorsque la lecture est terminée, sinon `false`.
- **`value`** -- un tableau typé d'octets : `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.
L'API Streams décrit également l'itération asynchrone sur `ReadableStream` avec la boucle `for wait..of`, mais elle n'est pas encore largement prise en charge (voir les [problèmes de navigateurs](https://github.com/whatwg/streams/issues/778#issuecomment-461341033)), nous utilisons donc la boucle `while`.
```

We receive response chunks in the loop, until the loading finishes, that is: until `done` becomes `true`.
Nous recevons des morceaux de réponse dans la boucle, jusqu'à ce que le chargement se termine, c'est-à-dire: jusqu'à ce que `done` devienne `true`.

To log the progress, we just need for every received fragment `value` to add its length to the counter.
Pour enregistrer la progression, nous avons juste besoin d'ajouter la longueur de la `value` de chaque fragment reçu au compteur.

Here's the full working example that gets the response and logs the progress in console, more explanations to follow:
Voici l'exemple de travail complet qui obtient la réponse et enregistre la progression dans la console, plus d'explications à suivre :

```js run async
// Step 1: start the fetch and obtain a reader
// Étape 1: démarrez la récupération et obtenir un lecteur
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
// Étape 2: obtenir la longueur totale
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)
// Étape 3: lecture des données
let receivedLength = 0; // reçu autant d'octets en ce moment
let chunks = []; // tableau de morceaux binaires reçus (comprend le corps)
while(true) {
const {done, value} = await reader.read();

Expand All @@ -68,45 +68,45 @@ while(true) {
console.log(`Received ${receivedLength} of ${contentLength}`)
}

// Step 4: concatenate chunks into single Uint8Array
// Étape 4: concaténer des morceaux en un seul 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
// Étape 5: décoder en une chaîne de caractères
let result = new TextDecoder("utf-8").decode(chunksAll);

// We're done!
// Nous avons fini !
let commits = JSON.parse(result);
alert(commits[0].author.login);
```

Let's explain that step-by-step:
Expliquons cela étape par étape :

1. We perform `fetch` as usual, but instead of calling `response.json()`, we obtain a stream reader `response.body.getReader()`.
1. Nous effectuons un `fetch` comme d'habitude, mais au lieu d'appeler `response.json() `, nous obtenons un lecteur de flux `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.
Veuillez noter que nous ne pouvons pas utiliser ces deux méthodes pour lire la même réponse : utilisez un lecteur ou une méthode de réponse pour obtenir le résultat.
2. Avant la lecture, nous pouvons déterminer la longueur complète de la réponse à partir de l'en-tête `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.
Il peut être absent pour les requêtes cross-origin (voir le chapitre <info:fetch-crossorigin>), techniquement, un serveur n'a pas à le configurer. Mais généralement, c'est à sa place.
3. Appel de `await reader.read()` jusqu'à ce que ce soit fait.

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.
Nous rassemblons des blocs de réponse dans le tableau `chunks`. C'est important, car une fois la réponse consommée, nous ne pourrons pas la "relire" à l'aide de `response.json()` ou d'une autre manière (vous pouvez essayer, il y aura une erreur).
4. À la fin, nous avons `chunks` -- un tableau de morceaux d'octets `Uint8Array`. Nous devons les joindre en un seul résultat. Malheureusement, il n'y a pas de méthode unique qui les concatène, donc il y a du code pour le faire :
1. Nous créons `chunksAll = new Uint8Array(receivedLength)` -- un tableau de même type avec la longueur combinée.
2. Ensuite nous utilisons la méthode `.set(chunk, position)` pour copier chaque `chunk` l'un après l'autre.
5. Nous avons le résultat dans `chunksAll`. C'est un tableau d'octets cependant, pas une chaîne de caractères.

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.
Pour créer une chaîne de caractères, nous devons interpréter ces octets. Le [TextDecoder](info:text-decoder) intégré fait exactement cela. Ensuite, nous pouvons `JSON.parse`, si nécessaire.

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:
Et si nous avions besoin d'un contenu binaire au lieu d'une chaîne de caractères ? C'est encore plus simple. Remplacez les étapes 4 et 5 par une seule ligne qui crée un `Blob` à partir de tous les morceaux :
```js
let blob = new Blob(chunks);
```

At we end we have the result (as a string or a blob, whatever is convenient), and progress-tracking in the process.
À la fin, nous avons le résultat (sous forme de chaîne de caractères ou d'objet blob, selon ce qui est pratique) et le suivi des progrès dans le processus.

Once again, please note, that's not for *upload* progress (no way now with `fetch`), only for *download* progress.
Encore une fois, veuillez noter que ce n'est pas pour la progression en *upload* (pas de possibilité actuellement avec `fetch`), seulement pour la progression en *download*.