6 gennaio 2021

Fetch

JavaScript può inviare richieste di rete al server e caricare nuove informazioni ogni volta che è necessario.

Per esempio, possiamo usare le richieste di rete per:

  • Inviare un ordine,
  • Caricare informazioni di un utente,
  • Ricevere gli ultimi aggiornamenti del server,
  • etc…

…e tutto senza alcun ricaricamento della pagina!

Ti sarà capitato di ascoltare o leggere il termine “AJAX” (acronimo di Asynchronous JavaScript And XML) che è comunemente utilizzato per accomunare (sotto un’unica effige) le richieste di rete in JavaScript. Non è però necessario usare XML: il termine proviene da un retaggio del passato ed è per questo che fa parte dell’abbreviazione.

Ci sono molti modi per inviare richieste di rete per richiedere informazioni dal server.

Il metodo fetch() è tra tutti il più moderno e versatile, e per questo inizieremo ad analizzare proprio questo. Questo metodo non è supportato dai browser più datati (ma è possibile risolvere con dei polyfills), ma lo è ampiamente tra quelli recenti.

La sintassi base è:

let promise = fetch(url, [options])
  • url – l’URL da raggiungere.
  • options – parametri opzionali: metodi, headers etc.

Senza options, questa è una semplice richiesta GET che scarica il contenuto di url.

Ottenere una risposta è comunemente un processo che si svolge in due fasi.

Possiamo valutare gli status HTTP dalle proprietà:

  • status – HTTP status code, ad esempio 200.
  • ok – boolean, true se l’HTTP status code è 200-299.

Per esempio:

let response = await fetch(url);

if (response.ok) { // se l'HTTP-status è 200-299
  // ricevi il body della risposta (il metodo sarà spiegato di seguito)
  let json = await response.json();
} else {
  alert("HTTP-Error: " + response.status);
}

Seconda fase: per prelevare il body della risposta, abbiamo bisogno di un ulteriore metodo.

Response fornisce molteplici metodi promise-based per accedere al body in svariati formati:

  • response.text() – legge il la risposta e ritorna un testo,
  • response.json() – interpreta e ritorna la risposta come un JSON,
  • response.formData() – ritorna la risposta come un oggetto (object) FormData (spiegato nel prossimo capitolo),
  • response.blob() – ritorna la risposta come Blob (binary data con type),
  • response.arrayBuffer() – ritorna la risposta come ArrayBuffer (rappresentazione low-level di binary data),
  • inoltre, response.body è un oggetto (object) ReadableStream, che consente di leggere il body “pezzo per pezzo” (chunk-by-chunk), come vedremo dopo in un esempio.

Ad esempio, otteniamo un oggetto (object) JSON con gli ultimi commit da GitHub:

let url = 'https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits';
let response = await fetch(url);

let commits = await response.json(); // legge il body della risposta e lo interpreta come JSON

alert(commits[0].author.login);

O facciamo lo stesso senza await, utilizzando la sintassi canonica delle promises:

fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits')
  .then(response => response.json())
  .then(commits => alert(commits[0].author.login));

Per ottenere il testo della risposta, await response.text() invece del .json():

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

let text = await response.text(); // legge il body della risposta come testo

alert(text.slice(0, 80) + '...');

Come caso d’uso per la lettura del binary format, richiediamo e mostriamo l’immagine del logo delle specifiche “fetch” (vedi il capitolo Blob per i dettagli sulle possibilità offerte dai Blob):

let response = await fetch('/article/fetch/logo-fetch.svg');

let blob = await response.blob(); // download del Blob object

// crea un tag <img>
let img = document.createElement('img');
img.style = 'position:fixed;top:10px;left:10px;width:100px';
document.body.append(img);

// mostra il logo
img.src = URL.createObjectURL(blob);

setTimeout(() => { // nascondi dopo tre secondi
  img.remove();
  URL.revokeObjectURL(img.src);
}, 3000);
Importante:

Possiamo solo scegliere un metodo di lettura del body.

Se per esempio abbiamo già prelevato il response con response.text(), successivamente response.json() non funzionerà, dato che il body sarà stato già processato.

let text = await response.text(); // elaborazione del response body
let parsed = await response.json(); // fallisce (già elaborato)

Headers della risposta (o response headers)

Le response headers sono disponibili nell’oggetto (object) Map-like response.headers.

In realtà non è esattamente un oggetto (object) Map, ma ha metodi molto simili per ottenere le singole header per nome o iterare tra tutte le headers:

let response = await fetch('https://api.github.com/repos/javascript-tutorial/en.javascript.info/commits');

// ricevo un header
alert(response.headers.get('Content-Type')); // application/json; charset=utf-8

// itero tra tutte le headers
for (let [key, value] of response.headers) {
  alert(`${key} = ${value}`);
}

Headers della richiesta (o request headers)

Per settare un header della request in fetch, possiamo usare la chiave headers dell’oggetto (object) passato come parametro delle opzioni, come ad esempio:

let response = fetch(protectedUrl, {
  headers: {
    Authentication: 'secret'
  }
});

…ci sono però una serie di HTTP headers proibiti, che non siamo autorizzati a settare:

  • Accept-Charset, Accept-Encoding
  • Access-Control-Request-Headers
  • Access-Control-Request-Method
  • Connection
  • Content-Length
  • Cookie, Cookie2
  • Date
  • DNT
  • Expect
  • Host
  • Keep-Alive
  • Origin
  • Referer
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade
  • Via
  • Proxy-*
  • Sec-*

Queste headers sono controllate esclusivamente dal browser perché aiutano a garantire una comunicazione HTTP corretta e sicura.

Richieste POST (o POST requests)

Per eseguire una richiesta POST o una richiesta con un altro metodo, possiamo usare le opzioni di fetch:

  • method – metodo HTTP, es. POST,
  • body – il body della richiesta, scegliendo tra:
    • una stringa (string) (es. JSON-encoded),
    • oggetto (object) FormData, per inviare i dati come form/multipart,
    • Blob/BufferSource per inviare binary data,
    • URLSearchParams, per inviare i dati in x-www-form-urlencoded encoding, anche se raramente utilizzato.

Il formato più comunemente utilizzato è il JSON.

Per esempio, il codice seguente invia l’oggetto (object) user come JSON:

let user = {
  name: 'John',
  surname: 'Smith'
};

let response = await fetch('/article/fetch/post/user', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  },
  body: JSON.stringify(user)
});

let result = await response.json();
alert(result.message);

Nota che, se il body della richiesta è una stringa (string), la Content-Type header è settata di default a text/plain;charset=UTF-8.

Se stiamo invece inviando un JSON, usiamo application/json come Content-Type corretto per i dati nelle opzioni headers.

Inviare un’immagine

Possiamo anche inviare binary data con fetch usando oggetti Blob o BufferSource.

In questo esempio, c’è un <canvas> sul quale possiamo disegnare spostarci sopra con il mouse. Al clic sul bottone “Invia” invieremo l’immagine al server:

<body style="margin:0">
  <canvas id="canvasElem" width="100" height="80" style="border:1px solid"></canvas>

  <input type="button" value="Invia" onclick="submit()">

  <script>
    canvasElem.onmousemove = function(e) {
      let ctx = canvasElem.getContext('2d');
      ctx.lineTo(e.clientX, e.clientY);
      ctx.stroke();
    };

    async function submit() {
      let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));
      let response = await fetch('/article/fetch/post/image', {
        method: 'POST',
        body: blob
      });

      // il server risponde con la conferma e la dimensione dell'immagine
      let result = await response.json();
      alert(result.message);
    }

  </script>
</body>

Nota che in questa occasione, invece, non impostiamo manualmente l’header Content-Type, perché un oggetto (object)Blob ha un tipo incorporato (in questo caso image/png, generato da toBlob). Per gli oggetti Blob, il tipo generato diventa il valore diContent-Type.

La funzione submit() può essere riscritta senza async/await come ad esempio:

function submit() {
  canvasElem.toBlob(function(blob) {
    fetch('/article/fetch/post/image', {
      method: 'POST',
      body: blob
    })
      .then(response => response.json())
      .then(result => alert(JSON.stringify(result, null, 2)))
  }, 'image/png');
}

Riepilogo

Una tipica richiesta fetch consiste in 2 chiamate await:

let response = await fetch(url, options); // ritorna le response headers
let result = await response.json(); // legge il body come JSON

O la versione senza await:

fetch(url, options)
  .then(response => response.json())
  .then(result => /* processa qui il result */)

Proprietà del response:

  • response.status – codice HTTP della risposta,
  • response.oktrue se lo status è 200-299.
  • response.headers – oggetto (object) Map-like con le HTTP headers.

Metodi per ricevere il response body:

  • response.text() – ritorna la risposta come testo,
  • response.json() – ritorna ed interpreta la risposta come oggetto (object) JSON,
  • response.formData() – ritorna la risposta come oggetto (object) FormData (per il form/multipart encoding, vedi il prossimo capitolo),
  • response.blob() – ritorna la risposta come oggetto (object) Blob (binary data con type),
  • response.arrayBuffer() – ritorna la risposta come oggetto (object) ArrayBuffer (low-level binary data),

Altre opzioni di fetch:

  • method – metodo HTTP,
  • headers – un oggetto (object) con le headers della richiesta (non tutte le headers sono concesse),
  • body – i dati da inviare (request body) come string o come oggetti FormData, BufferSource, Blob, UrlSearchParams.

Nei prossimi capitoli vedremo ulteriori opzioni e casi d’uso di fetch.

Esercizi

Crea la funzione async getUsers(names) che riceve un array di GitHub logins ed esegue il fetch degli utenti da GitHub. Infine ritorna l’array degli utenti stessi.

Usa l’url di GitHub per le informazioni degli utenti, indicando l’utente al posto del segnaposto USERNAME: https://api.github.com/users/USERNAME.

C’è un esempio di test nella sandbox.

Dettagli importanti:

  1. Ci dovrebbe essere una sola richiesta fetch per utente.
  2. Le richieste non dovrebbero essere bloccanti, così che i dati possano arrivare il prima possibile.
  3. Se una richiesta fallisce, o se non esiste tale utente, la funzione dovrebbe restituire null nell’array dei risultati.

Apri una sandbox con i test.

Per eseguire il fetch di un utente usa: fetch('https://api.github.com/users/USERNAME').

Se lo status del response è 200, chiama .json() per leggere l’oggetto (object) JS.

Altrimenti, se il fetch dovesse fallire, o lo status della risposta non è 200, ritorna null nell’array dei risultati.

So here’s the code:

async function getUsers(names) {
  let jobs = [];

  for(let name of names) {
    let job = fetch(`https://api.github.com/users/${name}`).then(
      successResponse => {
        if (successResponse.status != 200) {
          return null;
        } else {
          return successResponse.json();
        }
      },
      failResponse => {
        return null;
      }
    );
    jobs.push(job);
  }

  let results = await Promise.all(jobs);

  return results;
}

Nota che: la chiamata .then è agganciata direttamente al fetch, cosi che quando riceveremo una risposta, non ci sarà attesa per le altre fetches ma inizierà immediatamente la lettura di .json().

Se invece usassimo await Promise.all(names.map(name => fetch(...))) e chiamassimo .json() sui risultati, dovremmo attendere che tutte le fetches rispondano. Aggiungendo direttamente .json() ad ogni fetch invece ci assicureremo che ogni singolo fetch inizi la lettura dei dati come JSON senza attendere le altre.

Questo è un esempio di come l’API low-level Promise possa ancora essere utile anche se usiamo principalmente async/await.

Apri la soluzione con i test in una sandbox.

Mappa del tutorial