Crear una página de fallback sin conexión

¿Qué tienen en común la aplicación Asistente de Google, la aplicación de Slack, la aplicación de Zoom y casi cualquier otra aplicación específica de plataforma en su teléfono o computadora? Exacto, siempre al menos te dan algo. Incluso cuando no tengas una conexión de red, aún puedes abrir la aplicación Asistente, ingresar a Slack o iniciar en Zoom. Es posible que no obtengas nada particularmente significativo o incluso no puedas lograr lo que querías lograr, pero al menos obtienes algo y la aplicación tiene el control.

Aplicación móvil Asistente de Google sin conexión
Asistente de Google.

Aplicación de Slack móvil sin conexión
Slack.

Aplicación móvil de Zoom sin conexión
Zoom.

Con aplicaciones específicas de plataforma, incluso cuando no tienes una conexión de red, nunca obtendrás nada.

Por el contrario, en la Web, tradicionalmente no obtienes nada cuando estás desconectado. Chrome te ofrece el juego de dinosaurios sin conexión, pero eso es todo.

Aplicación móvil de Google Chrome que muestra el juego de dinosaurios sin conexión.
Google Chrome para iOS.

Aplicación de escritorio de Google Chrome que muestra el juego de dinosaurios 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 fallback 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 (almacenamiento en caché), puedes proporcionar una experiencia fuera de línea personalizada para tus usuarios. Esta puede ser una simple página con tu marca con la información de que el usuario está actualmente sin conexión, pero también puede ser una solución más creativa, como por ejemplo, el famoso juego de laberintos sin conexión de trivago con un botón de reconexión manual y un intento de reconexión automática usando una cuenta regresiva.

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

Registrar al service worker

Para poder hacer que esto suceda es a través de un service worker. Puedes registrar un service worker desde tu página principal como en el ejemplo de código a continuación. Por lo general, haces esto una vez que se haya cargado la aplicación.

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 en el ejemplo siguiente deberían aclarar las cosas. La idea central es hacer un pre-caché de un archivo llamado offline.html que se sirve sólo se presentan en las solicitudes de navegación en su defecto, y para permitir que el navegador maneja 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.
*/

// Al incrementar el  OFFLINE_VERSION obligará a lanzar el evento de instalación y
// los caché anteriores serán actualizados desde la red.
// Esta variable está declarada intencionalmente y no se usa.
// Agrega un comentario para tu linter si lo deseas:
// eslint-disable-next-line no-unused-vars
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Modifica esto con una diferente URL si es necesario.
const OFFLINE_URL = "offline.html";

self.addEventListener("install", (event) => {
  event.waitUntil(
    (async () => {
      const cache = await caches.open(CACHE_NAME);
      // Al definir {cache: 'reload'} en la nueva consulta asegurara que la
      // respuesta no sea desde el caché de HTTP; i.e., esta será
      // de la red.
      await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
    })()
  );
  // Obliga al service worker que espera a que se convierta en uno activo.
  self.skipWaiting();
});

self.addEventListener("activate", (event) => {
  event.waitUntil(
    (async () => {
      // Permite la navegación precargada si tiene compatibilidad
      // Mira https://developers.google.com/web/updates/2017/02/navigation-preload
      if ("navigationPreload" in self.registration) {
        await self.registration.navigationPreload.enable();
      }
    })()
  );

  // Le dice al service worker activo que tome el control inmediato de la página.
  self.clients.claim();
});

self.addEventListener("fetch", (event) => {
  // Solo queremos llamar al event.respondWith() si es una solicitud de navegación
  // para una página HTML.
  if (event.request.mode === "navigate") {
    event.respondWith(
      (async () => {
        try {
          // Primero, utiliza una respuesta de precarga de navegación.
          const preloadResponse = await event.preloadResponse;
          if (preloadResponse) {
            return preloadResponse;
          }

          // Siempre usa la red primero.
          const networkResponse = await fetch(event.request);
          return networkResponse;
        } catch (error) {
          // El catch solo se dispara cuando se obtiene una excepción
          // gracias a un error en la red.
          // Si fetch() regresa una respuesta HTTP valida con un codigo de respuesta en el
          // rango de 4xx o 5xx, el catch() no se llamará
          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;
        }
      })()
    );
  }

  // si nuestra condición de if() es falso, el controlador de fetch no atrapará la
  // solicitud. Si hay más controladores de fetch registrados, ellos tendrán la
  // oportunidad de llamar a event.respondWith(). De lo contrario, si no hay, no se llamará a
  // event.respondWith(), la solicitud será controlada por el buscador como si no
  // los service worker no se hubieran involucrado.
});

La página de respaldo sin conexión

El archivo de offline.html es donde puedes ser creativo y adaptarlo a tus necesidades y agregar tu marca. El siguiente ejemplo muestra lo mínimo de lo que es posible. Demuestra tanto la recarga manual basada en la pulsación de un botón como la recarga automática basada en el evento en online y el sondeo regular 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>Estás desconectado</h1>

    <p>Haz clic en el botón para intentar una recarga.</p>
    <button type="button">⤾ Recargar</button>

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

      // Escucha los cambios en la red, se recargará cuando se esté conectado.
      // Esto maneja el caso cuando el dispositivo este completamente fuera de linea.
      window.addEventListener('online', () => {
        window.location.reload();
      });

      // Checa si el servidor está respondiendo y recarga la página si lo esté.
      // Esto maneja el caso de cuando el dispositivo esté en linea, pero el servidor
      // esté fuera de linea o con problemas.
      async function checkNetworkAndReload() {
        try {
          const response = await fetch('.');
          // Verifica si tenemos una respuesta valida del servidor
          if (response.status >= 200 && response.status < 500) {
            window.location.reload();
            return;
          }
        } catch {
          // No se puede conectar al servidor, ignorar.
        }
        window.setTimeout(checkNetworkAndReload, 2500);
      }

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

Demostración

Puedes ver la página fallback sin conexión en acción en la siguiente demostración incrustada a continuación. Si estás interesado, puedes explorar el código fuente en Glitch.

Hincapié en hacer tu app instalable

Ahora que tu sitio tiene una página fallback sin conexión, es posible que te preguntes acerca de los próximos pasos. Para que tu aplicación se pueda instalar, debes de agregar un manifiesto de la aplicación web y, opcionalmente, idear una estrategia de instalación.

Hincapié en servir una página fallback sin conexión con Workbox.js

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

A continuación, aprenderás a definir una estrategia de instalación para tu aplicación.