Frontend

History API: a cosa servono e come si usano?

Autore

Manuel Ricci

Le History API sono un set di funzioni che un web developer deve conoscere. Senza, un singolo clic del pulsante "Indietro" e l'utente si ritroverà fuori dalla tua applicazione. Perderà tutto il lavoro in corso e sarà costretto a iniziare da capo.

Fortunatamente, una soluzione a questo problema esiste ed è anche abbastanza facile da usare. Da quando è stato formalizzato HTML5, abbiamo a disposizione le History API che ci permettono di aggiungere elementi alla cronologia e rispondere al clic del pulsante "Indietro" o "Avanti" del browser (le freccettine in alto a sinistra).

In questo articolo vedrai nel dettaglio di cosa sto parlando e, se vorrai, potrai usare il codice che riporterò per sviluppare qualcosa (condividilo con me se vuoi, sarò felice di menzionarti a fine articolo).

Prima però di procedere oltre, una doverosa introduzione al problema.

Reset dell'applicazione

Quando il web cominciò a diventare di uso comune, l'azione principale che doveva essere compiere era una, mostrare cose. Quando volevi vedere altro, cliccavi un link e andavi su una nuova pagina. Fin qui nulla di strano.

Negli anni, però, JavaScript è diventato più potente e gli sviluppatori hanno realizzato che ogni pagina web poteva essere una applicazione a se stante. Una pagina web poteva competere con un'applicazione desktop. Il problema però che impediva questo scontro leggendario erano i pulsanti "Avanti" e "Indietro" del browser.

Se un utente avesse eseguito una serie di task, si sarebbe aspettato che cliccando il pulsante "Indietro" sarebbe tornato indietro di uno step e invece sarebbe tornato alla pagina precedente, perdendo il lavoro svolto fino a quel momento.

Spesso, questo comportamento non era percettibile (lo è ancora oggi). Per esempio, consideriamo un'applicazione che usa l'oggetto XMLHttpRequest per recuperare nuovi dati e aggiornare la pagina. All'utente sembrerà di navigare su una nuova pagina, ma se cliccasse il pulsante "Indietro" andrebbe chissà dove.

Non sono stato abbastanza chiaro? Ti faccio un esempio...

Nel 1818 venne scritto un libro intitolato "Dictionnaire Infernal", un bestiario dove erano presenti i mostri delle superstizioni e delle storie mitologiche.

Ora quel libro è di dominio pubblico e qualcuno ha sviluppato un sito dove puoi "sfogliare" il bestiario, ma al posto di avere una pagina per ogni bestia ad ogni clic sui pulsanti "Avanti" e "Indietro" presenti in pagina (non del browser) verrà effettuata una chiama attraverso l'oggetto XMLHttpRequest che recupererà le informazioni della slide successiva. JavaScript poi si occuperà di aggiornare il box contente le informazioni, senza la necessità di aggiornare il resto della pagina.

Ecco qui il sito, completo di supporto alle History API. Prova a cliccare "Previous" e "Next". Noterai che l'URL non viene aggiornato, ma se cliccherai sui bottoni "Avanti" e "Indietro" del browser (quelli in alto a sinistra) vedrai che verrai portato al bestia precedente o successiva, in base a dove ti trovi nella tua cronologia. Puoi addirittura fare refresh e salvare un determinato mostro nei preferiti, non perderai i progressi fatti.

Qui invece trovi la versione senza supporto alle History API, la versione old-school. I pulsanti avanti e indietro ti portano alla pagina precedente, il reload non funziona e l'aggiunta ai preferiti rieseguirà l'applicazione dall'inizio. Una vera scocciatura.

Chiaro il problema? Allora possiamo procedere

Prepariamo l'esempio

Prima di procedere con le History API (eh lo so, che sei impaziente, ma è un tutorial base, abbi pazienza, devo spiegare tutto) ecco qualche informazione di base per capire ciò che andrai ad implementare.

La pagina usa un numero per tenere traccia della slide che al momento sta mostrando. Quando clicchi "Avanti" o "Indietro" il codice cambia il numero della slide. Quindi usa la propria funzione goToSlide() per aggiornare la pagina.

La magia avviene proprio nella funzione goToSlide(). Esegue una richiesta asincrona usando l'oggetto XMLHttpRequest.

1var richiesta = new XMLHttpRequest();
2
3// Inizia una richiesta per la slide che vuoi.
4function goToSlide(slide) {
5  if (richiesta != null) {
6    richiesta.open("GET", "Slides/" + slide, true);
7    richiesta.onreadystatechange = newSlideReceived;
8    richiesta.send();
9  } else {
10    // C'è stato un problema. Ignora.
11  }
12}

Sta a te decidere come viene generato il contenuto della slide. Solitamente c'è qualche framework per il routing che attiva il codice server-side. Il codice in questione recupererà le informazioni dalla cache, dal database, da un file, o da qualunque tipo di sorgente e lo restituirà.

Qui le cose sono estremamente semplici. La richiesta recupera del codice HTML da un file statico. Quindi se richiedi la terza slide (Slides/3), otterrai il codice solo di quella slide.

Quando la richiesta termina, il browser eseguirà newSlideReceived(). A questo punto si tratta semplicemente di prendere il markup ricevuto e piazzarlo in pagina usando un div come placeholder.

1// Quando la slide è stata ricevuta, inseriscila in pagina.
2function newSlideReceived() {
3  if (richiesta.readyState == 4 && richiesta.status == 200) {
4    document.getElementById("slide").innerHTML = richiesta.responseText;
5  }
6}

Proprio questo è il modo in cui funziona la versione old-school del bestiario, quella senza il supporto alle History API.

L'oggetto history

L'oggetto history esiste dalla nascita di JavaScript, ma per la maggior parte della sua vita è stato molto meno potente e meno utile di quanto è oggi.

L'oggetto history originale aveva solo una proprietà e tre metodi. La proprietà era length e ti diceva quanti elementi erano presenti in cronologia. Abbastanza inutile.

I tre metodi originale erano:

  • back() -- Mandava l'utente indietro di uno step nella cronologia
  • forward() -- Mandava l'utente avanti di uno step nella cronologia (se possibile)
  • go() -- Mandava l'utente avanti o indietro di *n *step nella cronologia.

Utili solo se l'obiettivo era quello di creare dei pulsanti personalizzati per navigare la cronologia.

Le History API hanno aggiunti due cosine decisamente più succulente: il metodo pushState() e l'evento onPopState.

Aggiungere un nuovo elemento alla cronologia

Il metodo pushState() è la punta di diamante della History API. Ti permette di aggiungere nuovi elementi alla cronologia. Eccolo in azione:

Metodo pushState() in azione

A questo punto ti starai chiedendo: perché avere più voci della stessa pagina? Il trucco sta nel fatto che ogni voce ha il suo stato collegato ad essa, un pezzo d'informazione o oggetto che tu hai impostato.

Quando l'utente farà un passo indietro nella sua cronologia tale stato permetterà di tornare alla stessa pagina, ma nella sua versione precedente.

Il metodo pushState() prende tre argomenti:

  • Data -- Il primo argomento è qualunque dato tu voglia memorizzare che rappresenti lo stato attuale della pagina. Userai questa informazione per ripristinare la pagina quando l'utente navigherà nella cronologia.
  • Titolo -- Il secondo argomento è il titolo della pagina che vuoi che il browser mostri. Al momento, tutti i browser ignorano questo parametro, quindi puoi passare null.
  • URL -- Il terzo argomento è la nuova versione dell'URL che vuoi mostrare nella barra degli indirizzi. Questo ti permette di supportare al meglio il pulsante di reload e l'aggiunta ai preferiti.

Nell'esempio del Dictionnaire Infernal, tutto quello che c'è da tenere da conto è il numero della slide. Basta aggiungerlo alla cronologia ogni volta che la slide cambia.

Questa è la linea di codice da usare:

Aggiungere un elemento alla cronologia

history.pushState(slideNumber, null, null);

Come avrai già capito la riga di codice qui sopra si limita a memorizzare il numero della slide. Il titolo non viene passato (non avrebbe comunque funzionato), come anche l'URL.

Per completezza, ecco le due chiamate al metodo pushState() in posizione. In risposta al clic sui link "Avanti" e "Indietro" della pagina web.

1function nextSlide() {
2  var slide;
3  // Se è l'ultima pagina, vai alla prima
4  if (currentSlide == 4) {
5    slide = 1;
6  } else {
7    slide = currentSlide + 1;
8  }
9
10  history.pushState(slide, null, null);
11  goToSlide(slide);
12  return false;
13}
14
15function previousSlide() {
16  var slide;
17  // Se è la prima pagina, vai all'ultima.
18  if (currentSlide == 1) {
19    slide = 4;
20  } else {
21    slide = currentSlide - 1;
22  }
23
24  history.pushState(slide, null, null);
25  goToSlide(slide);
26  return false;
27}

Ritornare alla pagina precedente

Tutto molto bello, ma al momento ci siamo limitati ad aggiungere elementi alla cronologia, come facciamo a leggere i dati salvati? Beh... con l'evento onPopState.

L'evento onPopState viene eseguito ad ogni navigazione della cronologia, inclusi i metodi back() e forward() menzionati in precedenza, oltre ai pulsanti del browser.

L'evento onPopState fornisce le informazioni memorizzate in precedenza con il metodo pushState(). Nel nostro caso, il numero della slide.

Il tuo compito è quello di recuperare quell'informazione e ripristinare la pagina alla versione desiderata. Nell'esempio che stiamo portando avanti ci basta chiamare la funzione goToSlide(), la quale gestisce la navigazione tra le slide. Il codice completo è il seguente:

1// Viene eseguito quando l'utente va avanti e indietro nella cronologia.
2window.onpopstate = function (e) {
3  if (e.state != null) {
4    goToSlide(e.state);
5  }
6};

Mi raccomando, controlla che e.state sia diverso da null, serve a capire se la pagina è stata caricata per la prima volta o no.

Cambiare l'URL

Perché una cosa così figa la lascio per ultima? Perché non è per tutti...

Nel bestiario che supporta le History API non funziona il bottone "Ricarica" e nemmeno l'aggiunta ai preferiti, entrambi ti porteranno sempre all'inizio.

La soluzione a questo problema è cambiare l'URL. Potrebbe capitare che in seguito ad un refresh, l'utente possa visualizzare un 404 oppure gli può essere mostrata una pagina leggermente diversa. Se non sai come gestire URL differenti, non c'è vergogna ad usare la versione semplificata, anche se il mio consiglio è di imparare a farlo, non essere pigro.

Come dici? Non sei pigro? Bene, allora te lo spiego direttamente io

Di base di sono due approcci:

  • Cambiare URL, tipo con Slide1.html, Slide2.html e così via per tutte le slide. Il problema è che bisogna averle queste pagine da qualche parte, oppure devi mettere in piedi un router che gestisca queste richieste lato server
  • Mantenere lo stesso URL, ma aggiungere i parametri. L'approccio più semplice è quello di usare i parametri (la parte dopo il punto di domanda negli URL). Gli URL saranno i seguenti SlideShow.html?slide=1, SlideShow.html?slide=2, ecc. Poco codice, ma estramamente efficace. Niente pagine in base al numero di slide, niente routing, niente codice lato server, solo una riga di JavaScript.

Ecco quindi la versione modificata, seguendo il secondo approccio, del pushState di prima:

1history.pushState(slide, null, "SlideShow.html?slide=" + slide);

Ultimissima parte prima del recap finale. Quando pagina viene ricaricata, devi controllare i parametri nell'URL e, se presenti, chiamare la funzione goToSlide().

1window.onload = function () {
2  var urlParams = new URLSearchParams(window.location.search);
3
4  if (urlParams.has("slide")) {
5    var slideParam = Number(urlParams.get("slide"));
6    if (!isNaN(slideParam)) {
7      goToSlide(slideParam);
8    }
9  }
10};

Ecco il Dictionnaire Infernal completo anche di questa nuova funzione.

Recap finale

Le History API sono facili da usare, ma se usate male, possono creare più problemi di quelli che risolvono. Quindi ecco un piccolo recap:

  • Il dinamico duo. Il metodo pushState() e l'evento onPopState sono partner. Quello che inserisci con uno devi tirarlo fuori con l'altro.
  • Non cambiare URL a meno che non sai come gestirlo. Fichissimo cambiare URL, ma bisogna essere in grado di gestire con efficienza tale cambiamento, altrimenti si rischia di rompere tutto
  • Non fermarti al semplice numero. Nel primo argomento del metodo onPushState() puoi memorizzare anche oggetti complessi che possono aiutarti nel completamente di task più complesse.

Caricamento...

Diventiamo amici di penna? ✒️

Iscriviti alla newsletter per ricevere una mail ogni paio di settimane con le ultime novità, gli ultimi approfondimenti e gli ultimi corsi gratuiti puubblicati. Ogni tanto potrei scrivere qualcosa di commerciale, ma solo se mi autorizzi, altrimenti non ti disturberò oltre.

Se non ti piace la newsletter ti ricordo la community su Discord, dove puoi chiedere aiuto, fare domande e condividere le tue esperienze (ma soprattutto scambiare due meme con me). Ti aspetto!

Ho in previsione di mandarti una newsletter ogni due settimane e una commerciale quando capita.