Creare una pagina di riserva offline

Cosa hanno in comune l'app Assistente Google, l'app Slack, l'app Zoom e quasi qualsiasi altra app specifica per la piattaforma sul tuo smartphone o computer? Giusto, ti danno sempre almeno qualcosa. Anche se non hai una connessione di rete, puoi comunque aprire l'app Assistente, accedere a Slack o avviare Zoom. Potresti non ottenere nulla di particolarmente significativo o persino non riuscire a ottenere ciò che volevi, ma almeno ottieni qualcosa e l'app è in controllo.

App mobile dell'Assistente Google quando è offline.
Assistente Google.

App mobile Slack in modalità offline.
Slack.

App mobile Zoom in modalità offline.
Zoom.

Con le app specifiche per la piattaforma, non ottieni mai nulla, anche quando non hai una connessione di rete.

Al contrario, sul web, in genere non ricevi nulla quando sei offline. Chrome ti offre il gioco del dinosauro offline, ma non di più.

App mobile Google Chrome che mostra il gioco Dino offline.
Google Chrome per iOS.

App desktop Google Chrome che mostra il gioco Dino offline.
Google Chrome per macOS.

Sul web, se non hai una connessione di rete, per impostazione predefinita non ricevi nulla.

Una pagina di riserva offline con un service worker personalizzato

Tuttavia, non deve necessariamente essere così. Grazie ai service worker e all'API Cache Storage, puoi offrire ai tuoi utenti un'esperienza offline personalizzata. Può trattarsi di una semplice pagina con il brand che indica che l'utente è attualmente offline, ma può anche essere una soluzione più creativa, come ad esempio il famoso gioco del labirinto offline di trivago con un pulsante Ricollega manuale e un conto alla rovescia per il tentativo di ricollegamento automatico.

La pagina offline di trivago con il labirinto offline di trivago.
Il labirinto offline di trivago.

Registrazione del service worker

Per farlo, puoi utilizzare un service worker. Puoi registrare un worker di servizio dalla tua pagina principale, come nell'esempio di codice riportato di seguito. In genere, lo fai una volta caricata l'app.

window.addEventListener("load", () => {
  if ("serviceWorker" in navigator) {
    navigator.serviceWorker.register("service-worker.js");
  }
});

Il codice del service worker

I contenuti del file del servizio worker effettivo potrebbero sembrare un po' complicati a prima vista, ma i commenti nell'esempio di seguito dovrebbero chiarire le cose. L'idea di base è memorizzare nella cache un file denominato offline.html che viene pubblicato solo per le richieste di navigazione non andate a buon fine e lasciare che il browser gestisca tutti gli altri casi:

/*
Copyright 2015, 2019, 2020, 2021 Google LLC. All Rights Reserved.
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 http://www.apache.org/licenses/LICENSE-2.0
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
*/

// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
// This variable is intentionally declared and unused.
// Add a comment for your linter if you want:
// eslint-disable-next-line no-unused-vars
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = "offline.html";

self.addEventListener("install", (event) => {
  event.waitUntil(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      // Setting {cache: 'reload'} in the new request ensures that the
      // response isn't fulfilled from the HTTP cache; i.e., it will be
      // from the network.
      await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
    })()
  );
  // Force the waiting service worker to become the active service worker.
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      // Enable navigation preload if it's supported.
      // See https://developers.google.com/web/updates/2017/02/navigation-preload
      if ("navigationPreload" in self.registration) {
        await self.registration.navigationPreload.enable();
      }
    })()
  );

  // Tell the active service worker to take control of the page immediately.
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  // Only call event.respondWith() if this is a navigation request
  // for an HTML page.
  if (event.request.mode === "navigate") {
    event.respondWith(
      (async () => {
        try {
          // First, try to use the navigation preload response if it's
          // supported.
          const preloadResponse = await event.preloadResponse;
          if (preloadResponse) {
            return preloadResponse;
          }

          // Always try the network first.
          const networkResponse = await fetch(event.request);
          return networkResponse;
        } catch (error) {
          // catch is only triggered if an exception is thrown, which is
          // likely due to a network error.
          // If fetch() returns a valid HTTP response with a response code in
          // the 4xx or 5xx range, the catch() will NOT be called.
          console.log("Fetch failed; returning offline page instead.", error);

          const cache = await caches.open(CACHE_NAME);
          const cachedResponse = await cache.match(OFFLINE_URL);
          return cachedResponse;
        }
      })()
    );
  }

  // If our if() condition is false, then this fetch handler won't
  // intercept the request. If there are any other fetch handlers
  // registered, they will get a chance to call event.respondWith().
  // If no fetch handlers call event.respondWith(), the request
  // will be handled by the browser as if there were no service
  // worker involvement.
});

La pagina di riserva offline

Nel file offline.html puoi creare la creatività, adattarla alle tue esigenze e aggiungere il tuo branding. L'esempio seguente mostra il minimo indispensabile di ciò che è possibile. Mostra sia il ricaricamento manuale in base alla pressione di un pulsante sia il ricaricamento automatico in base all'evento online e al polling regolare del server.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <title>You are offline</title>

    <!-- Inline the page's stylesheet. -->
    <style>
      body {
        font-family: helvetica, arial, sans-serif;
        margin: 2em;
      }

      h1 {
        font-style: italic;
        color: #373fff;
      }

      p {
        margin-block: 1rem;
      }

      button {
        display: block;
      }
    </style>
  </head>
  <body>
    <h1>You are offline</h1>

    <p>Click the button below to try reloading.</p>
    <button type="button">⤾ Reload</button>

    <!-- Inline the page's JavaScript file. -->
    <script>
      // Manual reload feature.
      document.querySelector("button").addEventListener("click", () => {
        window.location.reload();
      });

      // Listen to changes in the network state, reload when online.
      // This handles the case when the device is completely offline.
      window.addEventListener('online', () => {
        window.location.reload();
      });

      // Check if the server is responding and reload the page if it is.
      // This handles the case when the device is online, but the server
      // is offline or misbehaving.
      async function checkNetworkAndReload() {
        try {
          const response = await fetch('.');
          // Verify we get a valid response from the server
          if (response.status >= 200 && response.status < 500) {
            window.location.reload();
            return;
          }
        } catch {
          // Unable to connect to the server, ignore.
        }
        window.setTimeout(checkNetworkAndReload, 2500);
      }

      checkNetworkAndReload();
    </script>
  </body>
</html>

Demo

Puoi vedere la pagina di riserva offline in azione nella demo incorporata di seguito. Se ti interessa, puoi esplorare il codice sorgente su Glitch.

Nota a margine sull'installazione dell'app

Ora che il tuo sito ha una pagina di riserva offline, potresti chiederti quali sono i passaggi successivi. Per rendere installabile la tua app, devi aggiungere un file manifest dell'app web e, facoltativamente, definire una strategia di installazione.

Nota a margine sulla pubblicazione di una pagina di riserva offline con Workbox.js

Potresti aver sentito parlare di Workbox. Workbox è un insieme di librerie JavaScript per aggiungere il supporto offline alle app web. Se preferisci scrivere meno codice di worker di servizio, puoi utilizzare la ricetta Workbox per una pagina solo offline.

Ora scopri come definire una strategia di installazione per la tua app.