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:
- il metodo, POST nel nostro caso
- gli headers, soltanto il tipo mime nell'esempio
- il corpo del messaggio che qui sotto otteniamo rappresentando in JSON un oggetto che già abbiamo
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);
});