Creare una Single Page Application con hash routing in JavaScript

Lo sviluppo di interfacce web moderne punta sempre più verso la dinamicità e l’immediatezza. Una Single Page Application (SPA) permette di aggiornare solo porzioni della pagina senza ricaricare tutto il documento, offrendo un’esperienza più fluida. Uno dei metodi più semplici per ottenere questo risultato consiste nell’utilizzare il cambiamento dell’hash nell’URL per gestire la navigazione. L’articolo che segue illustra come costruire una piccola applicazione di questo tipo, che mostra un elenco di prodotti e consente di visualizzarne i dettagli, interamente lato client.

1. Recuperare i dati in modo asincrono

La base di ogni applicazione dinamica è la capacità di ottenere dati da una sorgente esterna. Qui viene definita una funzione asincrona che effettua una richiesta HTTP e restituisce un oggetto JavaScript a partire dal file JSON ricevuto.

async function getData(url = '') {
    const request = await fetch(url);
    return await request.json();
}

La semplicità di questa funzione nasconde un concetto fondamentale: l’uso di async/await consente di scrivere codice asincrono leggibile e lineare. È sufficiente passare un URL per ottenere la lista dei prodotti o altri dati che l’app deve mostrare.

2. Costruire la vista principale

La vista principale dell’app, o home, è il punto d’ingresso per l’utente. In questo blocco si imposta il titolo della pagina, si recuperano i dati e si costruisce una griglia di card con i prodotti da mostrare.

async function setHomeView(app) {
    document.title = 'JavaScript Hash Change';
    const items = await getData('all-products.json');
    const row = document.createElement('div');
    row.className = 'row';
    const itemsToShow = items.slice(0, 9);
    for (const item of itemsToShow) {
        const col = document.createElement('div');
        col.className = 'col-md-4 mb-3';
        col.innerHTML = `
            <div class="card">
                <figure><img src="images/${item.image}" alt="${item.name}"></figure>
                <div class="card-body">
                    <h5 class="card-title">${item.name}</h5>
                </div>
                <div class="card-footer">
                    <a href="#/products/${item.id}" class="btn btn-primary">View</a>
                </div>
            </div>
        `;
        row.appendChild(col);
    }
    app.innerHTML = row.outerHTML;
}

Ogni elemento della lista viene trasformato in una card contenente immagine, nome e link di dettaglio. Il link non apre una nuova pagina, ma aggiorna l’hash dell’URL, permettendo alla logica dell’app di reagire e cambiare vista.

3. Visualizzare il dettaglio del prodotto

Quando l’utente clicca su un link di dettaglio, l’URL cambia in qualcosa come #/products/3. La funzione seguente interpreta questo valore e genera dinamicamente il contenuto corrispondente.

async function setSingleView(app) {
    const urlHash = location.hash;
    if (!urlHash) {
        return setHomeView(app);
    }
    const items = await getData('all-products.json');
    const pathParts = urlHash.split('/');
    const itemId = parseInt(pathParts[pathParts.length - 1], 10);
    const item = items.find(i => i.id === itemId);
    if (!item) return;


    document.title = `${item.name} | JavaScript Hash Change`;
    let rating = '<div class="mt-3 mb-3 d-flex align-items-center">';
    for (let i = 0; i < item.rating; i++) {
      rating += '<i class="fa-solid fa-star pe-2"></i>';
    }
    rating += '</div>';

    const wrapper = document.createElement('div');
    wrapper.className = 'p-3';
    wrapper.innerHTML = `
    <h1 class="fw-bold">${item.name}</h1>
    ${rating}
    <div class="row mt-3">
        <figure class="col-md-5">
            <img src="images/${item.image}" alt="${item.name}" class="img-fluid">
        </figure>
        <article class="col-md-7">
            <div class="text-muted h4">$${item.price}</div>
            <p class="lead mt-3">${item.description}</p>
            <button class="btn btn-secondary mt-3 back-home">Back to Home</button>
        </article>
    </div>
    `;
    app.innerHTML = wrapper.outerHTML;
}

Questa sezione analizza l’hash per ricavare l’identificatore del prodotto, individua l’oggetto corrispondente e costruisce un layout completo con nome, prezzo, descrizione e valutazione. Il pulsante di ritorno permette di tornare alla home mantenendo la stessa pagina caricata.

4. Gestire il cambiamento dell’hash

Per rendere la navigazione interattiva, è necessario ascoltare i cambiamenti dell’hash nell’URL. Quando l’utente utilizza il pulsante “indietro” del browser o clicca su un link interno, l’app deve aggiornare la vista.

function handleHashChange(app) {
    window.addEventListener('hashchange', () => {
        setSingleView(app);
    });
}

Questo ascoltatore garantisce che la vista corretta venga caricata ogni volta che la parte finale dell’URL viene modificata, mantenendo il comportamento di una vera navigazione multipagina.

5. Tornare alla home senza ricaricare la pagina

Oltre a reagire al cambiamento dell’hash, l’app deve anche gestire i pulsanti interni che riportano l’utente alla home. La funzione seguente intercetta tutti i click e controlla se l’elemento cliccato corrisponde al pulsante di ritorno.

function handleNavigation(app) {
    document.addEventListener('click', evt => {
        const element = evt.target;
        if (element.classList.contains('back-home')) {
            setHomeView(app);
            const baseURL = location.href.replace(/#.*$/, '');
            history.replaceState('', document.title, baseURL);
        }
    }, false);
}

Grazie a questa soluzione, detta event delegation, non serve registrare un evento su ogni singolo pulsante: il documento intercetta tutti i click e reagisce solo quando serve. La rimozione dell’hash con history.replaceState ripristina l’URL pulito della home.

6. Avvio dell’applicazione

Infine, l’applicazione viene inizializzata non appena il documento è pronto. Il codice seguente identifica l’elemento principale che conterrà i contenuti e richiama tutte le funzioni necessarie.

function init() {
    const app = document.getElementById('app');
    setHomeView(app);
    setSingleView(app);
    handleNavigation(app);
    handleHashChange(app);
}

document.addEventListener('DOMContentLoaded', () => {
init();
});

In questo modo, se la pagina viene aperta con un hash già presente, la vista giusta viene caricata automaticamente, e gli eventi sono già attivi per gestire la navigazione interna.

7. Consigli per ampliare il progetto

Per chi desidera migliorare o ampliare questa base, ecco alcuni suggerimenti pratici:

  • Gestire gli errori di rete con try/catch e fornire messaggi di fallback all’utente.
  • Memorizzare i dati caricati in una variabile globale per evitare richieste ripetute.
  • Estendere il routing a percorsi più complessi, come categorie o filtri di ricerca.
  • Aggiungere un sistema di caricamento o “skeleton screen” mentre i dati vengono recuperati.
  • Adottare l’History API per ottenere URL più puliti e migliorare la compatibilità con i motori di ricerca.

Conclusione

L’approccio descritto dimostra come sia possibile creare una semplice SPA sfruttando esclusivamente le funzionalità native del browser. L’uso dell’hash come meccanismo di routing consente di simulare una navigazione multipagina pur restando su un’unica pagina HTML. Con pochi accorgimenti, questo modello diventa una base solida per progetti leggeri, cataloghi online o prototipi di interfaccia interattiva.

Torna su