¿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? Correcto, siempre al menos te dan algo. Aunque no tengas una conexión de red, puedes abrir la app del Asistente o ingresar Slack o inicia Zoom. Es posible que no obtengas nada particularmente significativo o que ni siquiera logres lo que querías lograr, pero al menos obtienes algo y la app está bajo control.
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.
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 para los 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.
Registra 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. Por lo general, lo haces una vez que se cargó la app.
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 publique 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 de lo que es 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 demo incorporada debajo. Si eres puedes explorar el código fuente en Error.
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 escuchado sobre 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 del service worker, puedes usar la receta de Workbox para solo la página sin conexión.
A continuación, aprenderás cómo definir una estrategia de instalación para tu app.