JavaScript

Rendere un sito utilizzabile anche offline: le basi del Service Worker

calendar_today personTeam EGSOFT schedule5 min di lettura
info

Disclaimer: le informazioni pubblicate in questa sezione hanno finalità di pura divulgazione tecnica. EG Software S.r.l. non si assume alcuna responsabilità per un utilizzo improprio dei contenuti, né per eventuali danni diretti o indiretti derivanti dalla loro applicazione, e non garantisce l'aggiornamento, l'accuratezza o la completezza delle informazioni riportate. Prima di utilizzare in produzione codice o procedure qui descritte, verificane sempre l'adeguatezza al proprio contesto.

Un Service Worker è uno script che il browser esegue in un thread separato dalla pagina, con la capacità di intercettare ogni richiesta di rete che la pagina effettua. È la tecnologia alla base delle Progressive Web App: permette di mettere in cache gli asset statici, servire contenuti anche senza connessione e, con altre API, ricevere notifiche push. Vediamo un'implementazione minima ma completa, pensata per un sito con poche pagine e risorse statiche stabili (CSS, JS, qualche immagine).

Registrare il Service Worker dalla pagina

Il primo passo avviene nello script della pagina normale, non nel Service Worker stesso: si verifica il supporto del browser e si registra il file che conterrà la logica di cache:

if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js')
      .then((registrazione) => {
        console.log('Service Worker registrato:', registrazione.scope);
      })
      .catch((errore) => {
        console.error('Registrazione Service Worker fallita:', errore);
      });
  });
}

Precaricare gli asset in fase di installazione

Nel file sw.js, l'evento install è il momento giusto per scaricare e mettere in cache le risorse che sappiamo essere necessarie fin da subito. event.waitUntil dice al browser di attendere che la Promise si risolva prima di considerare l'installazione completata:

const NOME_CACHE = 'sito-v1';
const RISORSE_STATICHE = [
  '/',
  '/css/style.css',
  '/js/app.js',
  '/images/placeholder.png',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(NOME_CACHE)
      .then((cache) => cache.addAll(RISORSE_STATICHE))
      .then(() => self.skipWaiting()) // attiva subito, senza aspettare la chiusura delle vecchie schede
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((nomiCache) =>
      Promise.all(
        nomiCache
          .filter((nome) => nome !== NOME_CACHE) // rimuove le cache di versioni precedenti
          .map((nome) => caches.delete(nome))
      )
    ).then(() => self.clients.claim())
  );
});

Rispondere alle richieste: cache prima, rete come riserva

L'evento fetch si attiva per ogni richiesta della pagina. La strategia più semplice per asset statici è "cache-first con aggiornamento": se la risorsa è già in cache la si restituisce subito, altrimenti si va in rete e si salva il risultato per la prossima volta.

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return; // le mutazioni non vanno mai messe in cache

  event.respondWith(
    caches.match(event.request).then((rispostaCache) => {
      return rispostaCache || fetch(event.request).then((rispostaRete) => {
        if (rispostaRete.status === 200) {
          const copia = rispostaRete.clone(); // il body si legge una sola volta
          caches.open(NOME_CACHE).then((cache) => cache.put(event.request, copia));
        }
        return rispostaRete;
      }).catch(() => {
        // Nessuna cache, nessuna rete: per una navigazione mostriamo una pagina di cortesia
        if (event.request.mode === 'navigate') {
          return new Response(
            '<h1>Connessione non disponibile</h1><p>Riprova più tardi.</p>',
            { headers: { 'Content-Type': 'text/html' } }
          );
        }
      });
    })
  );
});

Un avvertimento importante

Questa strategia va bene per asset che cambiano raramente. Per dati che devono essere sempre aggiornati (un prezzo, una disponibilità) è meglio l'opposto — "rete prima, cache come riserva" — altrimenti l'utente rischia di vedere a lungo dati non più validi. Vale anche la pena ricordare che, durante lo sviluppo, un Service Worker mal configurato può "incollare" una versione vecchia del sito nel browser: cambiare il nome della cache (sito-v2, v3...) a ogni deploy importante è il modo più semplice per forzare l'aggiornamento.

Altri articoli su JavaScript grid_viewTutte le tematiche
Scrivici su WhatsApp