JavaScript asincrono: da XMLHttpRequest a Fetch

Fetch ha ormai preso il posto di XMLHttpRequest, compagno fidato degli sviluppatori JavaScript sin dagli albori di AJAX. Rivediamoli insieme in questo post!

JavaScript asincrono: da XMLHttpRequest a Fetch

Questo post è un riadattamento tratto da Il Piccolo Libro di JavaScript, tutto quello che avresti voluto sapere sul linguaggio JavaScript (ma non hai mai osato chiedere).

Perché JavaScript è asincrono? Introduzione a XMLHttpRequest

Se dovessi rispondere alla domanda “ma a cosa serve davvero JavaScript?” direi che il motivo principale è uno: la possibilità di creare o rimuovere elementi HTML dinamicamente.

Immagina di dover creare una tabella HTML partendo da un array. Un array è una sorgente di dati sincrona: non c’è bisogno di “aspettare” che questi dati siano disponibili nel programma.

Spesso però vogliamo recuperare i dati da sorgenti esterne, come una REST API. Ma JavaScript è asincrono per natura. Se un’operazione può prendere più tempo del previsto allora è meglio delegare il lavoro a qualcun altro, che nel nostro caso è il browser. E’ così che ad esempio funzionano setTimeout e setInterval.

Ora, la comunicazione con una sorgente remota, per esempio una REST API, è sempre asincrona. Per dialogare con questo tipo di risorse il browser ci viene in aiuto mettendo a disposizione una browser API che esiste sin dagli albori: XMLHttpRequest.

Il nome di questo metodo potrebbe sembrare strano. In effetti l’origine di XMLHttpRequest va fatta risalire a quando i primi servizi web (REST API) erano scritti quasi tutti in Java e ritornavano dati in formato XML.

E’ anche da qui che prende il nome AJAX, acronimo per “Asynchronous JavaScript And XML”. Ma come vedremo più avanti JSON è diventato il formato universale per trasferire dati tra codice JavaScript ed API REST.

XMLHttpRequest in azione

Spazio ora ad un esercizio per esplorare meglio XMLHttpRequest. Crea un file di nome build-list.html e salvalo in un folder a tua scelta:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript asincrono</title>
</head>
<body>

</body>
<script src="ajax.js"></script>
</html>

Per creare una nuova richiesta AJAX è sufficiente chiamare new XMLHttpRequest. Sempre nello stesso folder crea un file di nome ajax.js ed inizialo così:

"use strict";

const request = new XMLHttpRequest();

Questa call (detta constructor call) ci da accesso all’oggetto request che invece di accettare una callback come argomento, come nel caso di setTimeout:

setTimeout(callback, 10000)

function callback(){
    console.log('hello timer!')
}

si aspetta di trovare la callback passata come valore a request.onload:

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  console.log("Got the response!");
}

load è un particolare tipo di evento ed ogni evento del DOM viene associato ad una callback che ne gestisce il destino.

Fatto questo possiamo configurare la richiesta con open(). Questa funzione accetta un metodo HTTP seguito dall’url a cui vogliamo indirizzare la richiesta (oltre ad altri argomenti opzionali che qui ometto):

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  console.log("Got the response!");
}

request.open("GET", "https://jsonplaceholder.typicode.com/posts");

Infine possiamo lanciare la richiesta con send():

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  console.log("Got the response!");
}

request.open("GET", "https://jsonplaceholder.typicode.com/posts");
request.send();

Se hai seguito tutti i passaggi questo codice dovrebbe produrre nella console del browser:

"Got the response!"

Ma stampare un messaggio serve a poco. Proviamo a fare qualcosa con la risposta che ci ha ritornato la REST API!

XMLHttpRequest e JSON

All’interno della callback che gestisce l’evento load abbiamo accesso all’oggetto XMLHttpRequest, disponibile come this. Questo oggetto contiene la risposta del server su this.response:

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  // this si riferisce alla richiesta HTTP corrente
  console.log(this.response);
}

request.open("GET", "https://jsonplaceholder.typicode.com/posts");
request.send();

Dopo aver salvato il codice e fatto refresh della pagina vedrai qualcosa del genere:

JavaScript asincrono: XMLHttpRequest e JSON

La risposta è in formato testuale, non molto utile per i nostri scopi, ma possiamo trasformala in JSON configurando request.responseType:

"use strict";

const request = new XMLHttpRequest();

request.onload = callback;

function callback() {
  console.log(this.response);
}

request.open("GET", "https://jsonplaceholder.typicode.com/posts");
request.responseType = "json"; // trasformo la risposta in JSON
request.send();

Salva il codice, fai refresh della pagina e guarda nella console:

JavaScript asincrono: XMLHttpRequest e JSON

Finalmente abbiamo un array di oggetti, pronti da usare nella prossima sezione!

Generare liste di elementi HTML con JavaScript e interagire con l’API

jsonplaceholder.typicode.com è un simpatico sito web che offre una REST API fake con cui fare esperimenti. L’url (detto anche endpoint) che stiamo usando nel nostro esempio ritorna un array di 100 oggetti, con ogni oggetto composto da una serie di proprietà.

Un’array sembra un’ottima occasione per creare una lista di elementi HTML. Ma prima di andare avanti cerchiamo subito di organizzare il nostro codice. Ci sono diversi modi per strutturare JavaScript:

  • con i moduli
  • con prototype
  • con le classi

In questa sezione scriveremo una classe ES6 allo scopo di familiarizzare meglio con questo stile. Pensiamo però al design della classe. Uno sviluppatore che volesse usarla deve passare almeno tre argomenti al costruttore:

  • un url da cui recuperare i dati
  • un bersaglio su cui agganciare i nuovi elementi HTML
  • un numero che definisce quanti elementi vogliamo creare nella pagina

Penso a qualcosa del genere:

const target = document.querySelector("#list");
const url = "https://jsonplaceholder.typicode.com/posts";

const list = new HTMLList(url, target, 15);

Sembra una buona idea! Passiamo all’implementazione. Nel file ajax.js crea una classe HTMLList dotata di un costruttore e di un metodo che chiamiamo getData. count che sarebbe il numero di elementi desiderati può essere un default parameter ES6, ovvero un parametro con un valore di default:

"use strict";

class HTMLList {
  constructor(url, target, count = 20) {
    this.url = url;
    this.target = target;
    this.count = count;
  }

  getData() {
    return;
  }
}

In questo modo il codice non creerà 100 elementi anche se lo sviluppatore dimentica di passare il parametro count. Ora passiamo al metodo getData. Può contenere tutto il codice per la richiesta HTTP, quindi il codice che abbiamo scritto nella sezione precedente va copiato ed incollato all’interno di getData. Ma con qualche modifica. La callback che gestisce i dati diventa un metodo della classe, handleOnLoad:

"use strict";

class HTMLList {
  constructor(url, target, count = 20) {
    this.url = url;
    this.target = target;
    this.count = count;
  }

  getData() {
    const request = new XMLHttpRequest();
    // La callback che gestisce i dati diventa un metodo della classe
    request.onload = this.handleOnLoad;
    request.open("GET", this.url);
    request.responseType = "json";
    request.send();
  }

  handleOnLoad() {
    // Ecco la callback
    console.log(this.response);
  }
}

E che ne pensi di aggiungere un metodo per stampare gli elementi sulla pagina? Potrebbe chiamarsi render? Prima però facciamo in modo che handleOnLoad salvi da qualche parte la risposta del server:

// resto omesso per brevità

  constructor(url, target, count = 20) {
    this.url = url;
    this.target = target;
    this.count = count;
    // nuovo membro della classe
    this.data = [];
  }

// resto omesso per brevità

  handleOnLoad() {
    // salva la risposta dopo averla ricevuta
   this.data = this.response;
  }

ed ora implementiamo render. Stiamo usando ES6 quindi possiamo sfruttare for…of:

// resto omesso per brevità

  render() {
    let ul = document.createElement("ul");
    for (let element of this.data.slice(0, this.count)) {
      let li = document.createElement("li");
      let textNode = document.createTextNode(element.title);
      li.appendChild(textNode);
      ul.appendChild(li);
    }
    this.target.appendChild(ul);
  }

Sembra tutto a posto. Aggiungi un elemento nel file build-list.html ed assegnagli un id denominato list:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JavaScript asincrono</title>
</head>
<body>
<div id="list">
    <!-- qui stampiamo la lista -->
</div>
</body>
<script src="ajax.js"></script>
</html>

Vai su ajax.js e completa il codice in modo da creare un nuovo oggetto dalla classe e chiamare i due metodi:

// aggiungi questo codice al file
const target = document.querySelector("#list");
const url = "https://jsonplaceholder.typicode.com/posts";
const list = new HTMLList(url, target, 15);
list.getData();
list.render();

Ed ora testiamo il codice. Apri build-list.html nel browser e … pagina bianca! L’unico risultato è quello di aver creato un ul vuoto. Che cosa può esserci sfuggito?

JavaScript asincrono colpisce ancora. O no?

La famosa “natura asincrona di JavaScript” ha colto nel segno ancora una volta. Oppure no? Dai uno sguardo a getData:

  getData() {
    const request = new XMLHttpRequest();
    request.onload = this.handleOnLoad;
    request.open("GET", this.url);
    request.responseType = "json";
    request.send();
  }

ed in particolare a questa riga:

request.onload = this.handleOnLoad;

Nel momento in cui il motore JavaScript “capisce” che this.handleOnLoad è una callback la funzione non finisce nel Call Stack ma viene invece gestita dal browser. E se chiamo una funzione sincrona come render dopo getData non potrò mai aspettarmi sincronia tra le due funzioni:

list.getData();
list.render();

Ognuna di queste due funzioni va per conto suo per dirla in parole povere. La soluzione? Potresti essere tentato da questa modifica:

// resto omesso per brevità

  handleOnLoad() {
    this.data = this.response;
    this.render();
  }

  // resto omesso per brevità

const target = document.querySelector("#list");
const url = "https://jsonplaceholder.typicode.com/posts";
const list = new HTMLList(url, target, 15);
list.getData();
// non c'è più bisogno di chiamare list.render()

handleOnLoad ora ha il compito di chiamare this.render() ma JavaScript proprio non ne vuole sapere:

Uncaught TypeError: this.render is not a function
    at XMLHttpRequest.handleOnLoad (ajax.js:18)

this.render() non è una funzione perché il contesto in cui viene chiamata handleOnLoad è un oggetto XMLHttpRequest! Per tagliare la testa al toro possiamo sfruttare le arrow functions ES6, che non hanno un riferimento a this e si prestano bene per sostituire la callback handleOnLoad:

  getData() {
    const request = new XMLHttpRequest();
    // Sostituiamo this.handleLoad con una arrow function
    // e chiamiamo this.render all'interno della stessa
    // ora this punta alla classe
    request.onload = () => {
      this.render(request.response);
    };
    request.open("GET", this.url);
    request.responseType = "json";
    request.send();
  }

Ora non avrai più accesso a this.response ma poco male. A questo punto ha poco senso anche tenere un nome come getData, può essere sostituito da qualcosa come create dato che il consumatore della nostra classe non dovrà più chiamare render a mano. Ecco il codice completo, con le opportune modifiche:

class HTMLList {
  constructor(url, target, count = 20) {
    this.url = url;
    this.target = target;
    this.count = count;
  }

  create() {
    const request = new XMLHttpRequest();
    request.onload = () => {
      this.render(request.response);
    };
    request.open("GET", this.url);
    request.responseType = "json";
    request.send();
  }

  render(data) {
    let ul = document.createElement("ul");
    for (let element of data.slice(0, this.count)) {
      let li = document.createElement("li");
      let textNode = document.createTextNode(element.title);
      li.appendChild(textNode);
      ul.appendChild(li);
    }
    this.target.appendChild(ul);
  }
}

const target = document.querySelector("#list");
const url = "https://jsonplaceholder.typicode.com/posts";
const list = new HTMLList(url, target, 15);
list.create();

E dopo questa giostra di emozioni abbiamo la nostra lista:

Generare liste di elementi HTML con JavaScript e interagire con l'API

Ma finito un giro ne inizia un altro e nella prossima sezione vedremo l’alternativa moderna a XMLHttpRequest.

L’ evoluzione di JavaScript asincrono: da XMLHttpRequest a Fetch

L’uso di XMLHttpRequest che abbiamo visto sopra è molto banale e non tiene conto degli errori che possono verificarsi durante una richiesta HTTP. Ma gestire gli errori con XMLHttpRequest può diventare complicato e poco chiaro.

jQuery ha reso più pratiche le richieste AJAX aggiungendo nel tempo jQuery.get() e jQuery.getJSON(). Ma non era abbastanza. In contemporanea all’introduzione delle Promise nel 2015 è nata una nuova Web API per le richieste AJAX: Fetch.

Fetch fa parte delle Web API, quindi è una funzione che prendiamo in prestito dal browser. Non è inclusa nel linguaggio JavaScript di per sè. Fetch fa uso delle Promise e l’obiettivo che si pone è rendere più semplici le richieste HTTP asincrone.

Eccolo in azione:

fetch("https://jsonplaceholder.typicode.com/posts")
  .then(function(response) {
    return response.json();
  })
  .then(function(data) {
    console.log(data);
  });

Il motivo per cui usiamo then 2 volte è che Fetch ritorna dal server un oggetto Response inglobato in una Promise. Per trasformare la risposta in JSON dobbiamo chiamare response.json() che a sua volta ritorna un’altra Promise. Ma nel secondo then possiamo finalmente accedere ai dati.

Con queste informazioni in mano faremo un refactoring della nostra classe. Il metodo create può abbandonare il caro XMLHttpRequest in favore di Fetch. Apri il file ajax.js e modifica create da così:

  create() {
    const request = new XMLHttpRequest();
    request.onload = () => {
      this.render(request.response);
    };

    request.open("GET", this.url);
    request.responseType = "json";
    request.send();
  }

a così:

  create() {
    fetch(this.url)
      .then(response => {
        return response.json();
      })
      .then(data => {
        this.render(data);
      });
  }

Il codice completo usando Fetch sarà:

"use strict";

class HTMLList {
  constructor(url, target, count = 20) {
    this.url = url;
    this.target = target;
    this.count = count;
  }

  create() {
    fetch(this.url)
      .then(response => {
        return response.json();
      })
      .then(data => {
        this.render(data);
      });
  }

  render(data) {
    let ul = document.createElement("ul");
    for (let element of data.slice(0, this.count)) {
      let li = document.createElement("li");
      let textNode = document.createTextNode(element.title);
      li.appendChild(textNode);
      ul.appendChild(li);
    }
    this.target.appendChild(ul);
  }
}

const target = document.querySelector("#list");
const url = "https://jsonplaceholder.typicode.com/posts";
const list = new HTMLList(url, target, 15);
list.create();

E lanciando build-list.html avremo ancora la nostra lista ad aspettarci nel browser. Ci sarebbe ancora un po’ di lavoro da fare però perché Fetch non è molto intuitivo come potrebbe sembrare. Soprattutto quando si tratta di errori. Che cosa succede se l’utente fornisce un url inesistente?

Il comportamento di Fetch è singolare. La particolarità di Fetch è che un errore 500 non fa scattare una rejection. Sono considerati come errori solo quelli derivanti da problemi di rete o risoluzione DNS.

Per rendere il codice più robusto dobbiamo sempre controllare lo stato della risposta (questo esempio usa httpstat.us per simulare una condizione di errore):

fetch("http://httpstat.us/500")
  .then(function(response) {
    if (!response.ok) {
      throw Error(response.statusText);
    }
    return response;
  })
  .catch(function() {
    console.log("error");
  });

Solo in questo modo l’handler catch entrerà in azione. Lascio a te come esercizio il refactoring del metodo create per la gestione degli errori. E come bonus finale puoi migliorare ancora il codice usando async/await con try/catch al posto di then/catch.

Se vuoi guardare un po’ di live coding (1 oretta) gustati il video sotto dove con Cristiano (un mio ex studente) partiamo da XMLHttpRequest passando per Fetch, fino alla forma più moderna con async/await (ES7).

XMLHttpRequest e Fetch. In Conclusione

La tecnologia AJAX, Asynchronous JavaScript And XML, ha reso possibile la creazione di interfacce fluide ed interattive dove l’utente non è più costretto ad assistere ai classici refresh della pagina.

Questo accadeva nel lontano 1999. Gli sviluppatori possono contattare una REST API interagendo dal codice JavaScript, recuperare i dati e mostrarli all’utente, senza interrompere l’esperienza di navigazione. XMLHttpRequest è stato il compagno fidato degli sviluppatori JavaScript sin dagli inizi.

Negli anni tutti gli sviluppatori JavaScript hanno sperimentato la natura asincrona di questo linguaggio, scoprendo come funzioni asincrone e funzioni sincrone viaggiano su due “binari” diversi, senza mai incontrarsi. E’ compito dello sviluppatore strutturare il codice in modo che ad una chiamata asincrona corrispondano eventuali operazioni sincrone.

In questo post abbiamo visto l’evoluzione di JavaScript asincrono, dall’uso di XMLHttpRequest all’interno di una classe ES6 passando per il refactoring con Fetch.

Oggi Fetch ha ormai preso il posto di XMLHttpRequest (anche se si basa su quest’ultimo, “wrappando” la vecchia API con le Promise). Fetch è una browser API basata sulle Promise e come tale può essere usata sia con then/catch che con async/await. Ma particolare attenzione deve essere prestata alla gestione degli errori: Fetch non considera i codici 4xx e 5xx come errori bloccanti.

Sempre attuale la lettura di Handling Failed HTTP Responses With fetch() per approfondire l’argomento.

Grazie per aver letto! Rimani sempre sintonizzato su questo blog e sul mio canale Youtube!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.