Creare una pagina di riserva offline

Cos'hanno in comune l'app Assistente Google, l'app Slack, l'app Zoom e quasi qualsiasi altra app specifica della piattaforma sul tuo telefono o computer? Esatto, ti danno sempre almeno qualcosa. Anche se non disponi di una connessione di rete, puoi comunque aprire l'app Assistente, inserire Slack o avviare Zoom. Potresti non ottenere nulla di particolarmente significativo o addirittura non essere in grado di ottenere ciò che volevi ottenere, ma almeno ottieni qualcosa e l'app ha il controllo.

App mobile Assistente Google in modalità offline.
Assistente Google.

App mobile Slack in modalità offline.
Slack.

App mobile Zoom in modalità offline.
Zoom.

Con app specifiche della piattaforma, anche quando non disponi di una connessione di rete, non ricevi mai nulla.

Al contrario, sul Web, di solito non ricevi nulla quando sei offline. Chrome ti offre il gioco dinosauro offline, ma è tutto.

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

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

Sul web, per impostazione predefinita, quando non disponi di una connessione di rete, non ricevi nulla.

Una pagina di riserva offline con un service worker personalizzato

Ma non deve necessariamente essere così. Grazie ai service worker e all'API Cache Storage, puoi fornire ai tuoi utenti un'esperienza offline personalizzata. Può essere una semplice pagina correlata al brand con le informazioni che l'utente è attualmente offline, ma può anche essere una soluzione più creativa, come, ad esempio, il famoso labirinto di trivago offline con un pulsante Riconnetti manuale e un conto alla rovescia automatico per il tentativo di riconnessione.

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

Registrazione del service worker

Per farlo, è possibile utilizzare un service worker. Puoi registrare un service worker dalla pagina principale, come nell'esempio di codice riportato di seguito. In genere questa operazione è completata dopo il caricamento dell'app.

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

Il codice del service worker

I contenuti dell'effettivo file del service worker a prima vista potrebbero sembrare un po' impegnativi, ma i commenti nell'esempio riportato di seguito dovrebbero chiarire le cose. L'idea alla base è pre-memorizzare nella cache un file denominato offline.html che viene pubblicato solo in caso di richieste di navigazione non riuscite 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.
});

Pagina di riserva offline

Il file offline.html è il luogo in cui puoi liberare la creatività, adattarlo alle tue esigenze e aggiungere il tuo branding. L'esempio seguente mostra il minimo indispensabile di ciò che è possibile fare. Illustra il ricaricamento manuale basato sulla pressione di un pulsante e il ricaricamento automatico in base all'evento online e al normale polling 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 secondaria su come rendere installabile l'app

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

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

Probabilmente hai sentito parlare di Workbox. Workbox è un insieme di librerie JavaScript che consente di aggiungere il supporto offline alle app web. Se preferisci scrivere meno codice del service worker, puoi utilizzare la formula Workbox per una pagina offline.

Nel prossimo video scoprirai come definire una strategia di installazione per la tua app.