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

¿Qué tienen en común la app de Asistente de Google, la app de Slack, la app de Zoom y casi cualquier otra app específica de la plataforma en tu teléfono o computadora? Cierto, siempre te dan algo. Incluso si no tienes conexión de red, puedes abrir la app de 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 obtienes algo y la app está en control.

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

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

App de Zoom para dispositivos móviles sin conexión
Zoom.

Con las apps específicas de una plataforma, nunca recibirás nada, incluso cuando no tengas una conexión de red.

En cambio, en la Web, tradicionalmente, no obtienes nada cuando no tienes conexión. Chrome te brinda el juego del dinosaurio sin conexión, pero eso es todo.

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

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

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

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

Sin embargo, no tiene por qué 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 no tiene conexión en ese momento, pero también puede ser una solución más creativa, como, por ejemplo, el famoso juego de laberinto sin conexión de trivago con un botón Reconnect manual y una cuenta regresiva para un 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.

Cómo registrar el service worker

La forma de hacerlo es a través de un service worker. Puedes registrar un trabajador de servicio desde tu página principal, como en la siguiente muestra de código. Generalmente, debes hacer esto una vez que la app se carga.

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

El código de service worker

El contenido del archivo del servicio en sí puede parecer un poco complicado a primera vista, pero los comentarios del siguiente ejemplo deberían aclarar las cosas. La idea principal es almacenar en caché previamente un archivo llamado offline.html que solo se entrega en solicitudes de navegación fallidas y permitir que el navegador controle 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

En el archivo offline.html, puedes ser creativo y adaptarlo a tus necesidades, y agregar tu desarrollo de la marca. En el siguiente ejemplo, se muestra lo mínimo posible. Muestra la recarga manual basada en la presión de un botón y la recarga automática basada en el evento online y la sondeo del servidor normal.

<!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 hacer que tu app sea instalable

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 app web y, de manera opcional, crear una estrategia de instalación.

Nota al margen sobre la publicación 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 compatibilidad sin conexión a las apps web. Si prefieres escribir menos código de service worker, puedes usar la receta de Workbox para una página sin conexión únicamente.

A continuación, aprenderás cómo definir una estrategia de instalación para tu app.