Cómo crear una página de resguardo sin conexión

¿Qué tienen en común las apps del Asistente de Google, Slack, Zoom y casi cualquier otra app específica de la plataforma de tu teléfono o computadora? Cierto, siempre te dan algo como mínimo. Incluso cuando no tengas una conexión de red, puedes abrir la app del Asistente, ingresar a Slack o iniciar Zoom. Es posible que no obtengas nada particularmente significativo o que incluso no puedas lograr lo que querías, pero al menos obtendrás algo y la app estará en control.

App para dispositivos móviles de Asistente de Google sin conexión.
Asistente de Google.

App de Slack para dispositivos móviles sin conexión
Slack

Aplicación Zoom para dispositivos móviles sin conexión
Zoom.

Con las apps específicas de cada plataforma, incluso si no tienes una conexión de red, nunca obtendrás nada.

En cambio, en la Web, normalmente no se obtiene nada sin conexión. Chrome te ofrece el juego del dinosaurio sin conexión, pero eso es todo.

App para dispositivos móviles de Google Chrome que muestra el juego del dinosaurio sin conexión.
Google Chrome para iOS.

App de escritorio de Google Chrome que muestra el juego del dinosaurio sin conexión.
Google Chrome para macOS.

En la Web, cuando no tienes una conexión de red, no obtienes nada de forma predeterminada.

Una página de resguardo sin conexión con un service worker personalizado

Sin embargo, no tiene que ser así. Gracias a los service Workers y la API de Cache Storage, puedes proporcionar una experiencia sin conexión personalizada a tus usuarios. Puede ser una página de marca simple con la información de que el usuario se encuentra sin conexión, pero también puede ser una solución más creativa, como, por ejemplo, el famoso juego de laberinto sin conexión trivago con un botón Reconnect manual y una cuenta regresiva de intento de reconexión automática.

La página sin conexión de trivago con el laberinto sin conexión de trivago.
El laberinto sin conexión de Trivago.

Registra el service worker

La forma de hacerlo es a través de un service worker. Puedes registrar un service worker desde tu página principal como en la muestra de código que aparece a continuación. Generalmente, haces esto cuando la app se carga.

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

El código del service worker

El contenido del archivo del service worker real puede parecer un poco complicado a primera vista, pero los comentarios del siguiente ejemplo deberían aclarar las cosas. La idea principal es almacenar previamente en caché un archivo llamado offline.html que solo se entrega en las solicitudes de navegación fallidas y permitir que el navegador se encargue de todos los demás casos:

/*
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 página de resguardo sin conexión

Con el archivo offline.html, puedes usar tu creatividad, adaptarla a tus necesidades y agregar tu desarrollo de la marca. El siguiente ejemplo muestra lo mínimo de lo que es posible. Muestra la recarga manual (cuando se presiona un botón) y la recarga automática basada en el evento online y los sondeos regulares del servidor.

<!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>

Demostración

Puedes ver la página de resguardo sin conexión en acción en la demostración incorporada a continuación. Si te interesa, puedes explorar el código fuente en Glitch.

Nota al margen sobre cómo permitir la instalación de su aplicación

Ahora que tu sitio tiene una página de resguardo sin conexión, es posible que te preguntes cuáles son los próximos pasos. Para que tu app se pueda instalar, debes agregar un manifiesto de la app web y, de manera opcional, crear una estrategia de instalación.

Nota al margen sobre la entrega de una página de resguardo sin conexión con Workbox.js

Es posible que hayas oído hablar de Workbox. Workbox es un conjunto de bibliotecas de JavaScript para agregar soporte sin conexión a apps web. Si prefieres escribir menos código de service worker, puedes usar la receta de Workbox para solo página sin conexión.

A continuación, obtén información sobre cómo definir una estrategia de instalación para tu app.