FetchAPI

recuperare risorse remote con javascript

fetch & async

Parlando di comunicazione (e in genere di operazioni onerose dal punto di vista del tempo di completamento) spesso si usano delle tecniche asincrone. In questo contesto quello che vogliamo evitare è che il programma si blocchi in attesa che l'operazione venga completata. Per ovviare a questo problema una possibile via di uscita è fare in modo che la parte di programma che ha bisogno del risultato del lavoro lento venga sospesa e riprenda quando l'operazione è stata completata, in alternativa si può fornire una funzione da chiamare una volta che il risultato è disponibile. A ben guardare le differenze sono più di tipo sintattico che altro.

Fetch API è una modalità per accedere a risorse remote usando dei meccanismi asincroni in maniera semplificata. In questo capitolo faremo uso sia di fetch API che delle istruzioni async await di Javascript. Non è necessario usare async await ma ci semplifica non poco la scrittura dei programmi.

Per fare una richiesta remota è possibile usare il metodofetch() che ha come parametro l'indirizzo della risorsa che vogliamo e ritorna come risultato un oggetto di tipo Promise che di per sé indica una operazione che prima o poi verrà completata (è pur sempre una richiesta in rete... ha bisogno di qualche millesimo di secondo!) o fallirà.

È possibile utilizzare l'oggetto Promise ottenuto per indicare cosa va fatto una volta che l'operazione (la comunicazione in rete nel nostro caso) è terminata ma è più semplice utilizzare l'istruzione await.

let risposta = await fetch("https://x.y.x/risorsa");

await fa in modo che l'esecuzione del nostro script venga sospesa e riprenda soltanto quando la Promise viene completata (sarebbe a dire che la risorsa è arrivata nel caso della fetch). La gestione asincrona in pratica la fa il browser. Da tener presente però che se usiamo await la funzione che contiene questa istruzione va dichiarata come async per marcarla come asincrona.

L'oggetto Promise restituito da fetch() se tutto va bene si risolve in un oggetto di tipo Response quindi nell'esempio sopra risposta è un oggetto Response che tra gli altri ha un comodo metodo json() per convertire il corpo della risposta da JSON ad oggetto. Il metodo json() in effetti restituisce un oggetto Promise (asincrono perché il decoding potrebbe richiedere tempo) che in assenza di errori si risolve in un oggetto javascript che viene costruito in base al testo JSON presente nel corpo della risposta.

let oggetto = await risposta.json();

Mettendo insieme le parti, una semplice funzione che recupera un oggetto memorizzato su un server remoto potrebbe essere:


async function recuperaInformazioni() {
    let risposta = await fetch("https://x.y.z/risorsa.json");
    let libro = await risposta.json();
    console.log(libro.titolo);
}

Invio dei dati al server

Il metodo fetch() ha in realtà due parametri: il primo è la URL della risorsa che ci interessa (già visto nella sezione precedente) e il secondo è un oggetto che ci permette di specificare alcuni parametri della richiesta tra cui abbiamo:

method
un metodo di HTTP
headers
un oggetto che rappresenta i diversi headers che vogliamo inserire
body
il corpo della richiesta, può essere scritto in diversi formati tra cui anche una stringa di testo (utile per mandare contenuto JSON)

Un caso comune è quello in cui si vuole inviare un oggetto al server (ad esempio per poterlo archiviare) rappresentandolo come JSON nel corpo della richiesta. Una possibile soluzione è quella di creare un oggetto che descrive la richiesta specificando:

Nel chiamare fetch() fornisco come parametri la URL del servizio e i parametri della richiesta appena definiti.


async function inviaDati(oggettoDaInviare) {
    let parametriRichiesta = {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(oggettoDaInviare)
    };
    let risposta = await fetch("https://x.y.z/inserisci",parametriRichiesta);
    /* ... */
}

Gestione degli errori

Finora abbiamo gestito la comunicazione con fetch API considerando ingenuamente che vada tutto bene: il che però è poco realistico. L'istruzione await in caso che l'oggetto Promise che sta gestendo non si risolve positivamente solleva una eccezione che può essere gestita utilizzando il costrutto try/catch. L'oggetto che rappresenta il problema che si è verificato ha diverse proprietà, due delle quali potrebbero essere utili per capire la natura del problema stesso: name e message.


async function richiestaRealista(){
    try{
        let risposta = await fetch(URL_ORARI);
        let oggetto = await risposta.json();
        /* ... */
    }catch(e){
        console.log(e.name);
        console.log(e.message);
    }
}

Attenzione ad una cosa: se il server risposnde con un 404 questo non solleva eccezioni: la comunicazione si è conclusa e quello che abbiamo ottenuto è una risposta che magari indica un errore. Per testare la risposta del server possiamo usare il campo ok dell'oggetto Response restituito da fetch(). ok sarà true se la risposta ha come codice un numero nel range 200-299.

Per evere maggior dettaglio è possibile usare le proprietà status e statusText sempre della risposta.

Promise

In questo capitolo abbiamo usato async/await in pratica per non usare gli oggetti Promise, vediamo però come si potrebbero usare direttamente. La differenza sta nel come gestisce il fatto che le operazioni sono asincrone.

Un oggetto Promise rappresenta un compito che impiega tempo per essere completato e che può finir bene o meno, può trovarsi in tre stati:

pending
lo stato iniziale, è ancora al lavoro/in attesa
fulfilled
operazioni completate con successo
rejected
operazioni finite perché ci sono stati problemi

Una volta che abbiamo l'oggetto Promise con il metodo then() possiamo passare due funzioni (la seconda è opzionale) che verranno chiamate se le cose sono andate bene (la prima) o se le cose sono andate male (la seconda). Vediamo la sola richiesta di una risorsa remota usando fetch()


fetch("https://server.sito.it/risorsa")
  .then(
    // questa funzione viene chiamata se la richiesta è andata a buon fine
    risposta => {
      if (risposta.ok) {
        // elaboro la risposta
        console.log(risposta.body);
      } else {
        console.log('stato:', risposta.status, risposta.statusText);
      }
    },
    // questa funzione viene chiamata in caso di errore
    errore => {
      console.log('errore nella comunicazione:', errore);
    }
  );

Attenzione ad una cosa: per quanto riguarda fetch() se il server risponde (anche un 404) si considera comunque fullfilled.

Spesso lo scopo è quello di avere l'oggetto Javascript che il server ci ha mandato come JSON, in questo caso il lavoro non è ancora finito. Dobbiamo convertire il corpo in un oggetto: di nuovo una operazione asincrona (del resto l'altro modo di farlo che abbiamo visto usavaa await). Siccome stiamo lavorando con Promise basta aggiungere ancora un then():


fetch(URL)
  .then(
    risposta => {
      if (risposta.ok) {
        // elaboro la risposta: converto da json
        return risposta.json(); // che è di nuovo Promise
      } else {
        console.log('stato:', risposta.status, risposta.statusText);
      }
    },
    errore => {
      console.log('err comunicazione:', errore);
    }
  )
  .then(
    oggetto => {
      console.log(oggetto.lunghezza);
    },
    errore => {
      console.log('err JSON:', errore);
    }
  );

Complicato? però possiamo semplificare usando catch() in fondo alla catena per intercettare tutti gli errori, sia che vengono dal primo then che dal secondo se non sono stati gestiti cioè se non è stata fornita la seconda funzione al then().


fetch(URL)
  .then(
    risposta => {
      if (risposta.ok) {
        // elaboro la risposta: converto da json
        return risposta.json(); // che è di nuovo Promise
      } else {
        // visto che ci siamo considero anche questo un errore
        throw new Error('Risposta non OK');
      }
    }
  )
  .then(
    oggetto => {
      console.log(oggetto.lunghezza);
    }
  )
  .catch(errore => {
    console.log('err generale:', errore);
  });