Che cosa hanno in comune l'app Assistente Google, l'app Slack, l'app Zoom e quasi tutte le altre app specifiche per piattaforma sul tuo smartphone o computer? Giusto, ti danno sempre almeno qualcosa. Anche quando non hai 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 riuscire a raggiungere il tuo obiettivo, ma almeno ottieni qualcosa e l'app è sotto controllo.



Al contrario, sul web, in genere non ottieni nulla quando sei offline. Chrome ti offre il gioco del dinosauro offline, ma non altro.


Una pagina di riserva offline con un service worker personalizzato
Tuttavia, non deve necessariamente essere così. Grazie ai service worker e all'API Cache Storage, puoi offrire un'esperienza offline personalizzata per i tuoi utenti. Può trattarsi di una semplice pagina brandizzata con le informazioni che l'utente è attualmente offline, ma può anche essere una soluzione più creativa, come, ad esempio, il famoso gioco del labirinto offline di trivago con un pulsante manuale Riconnetti e un conto alla rovescia per il tentativo di riconnessione automatica.

Registrazione del service worker
Per farlo, devi utilizzare un service worker. Puoi registrare un service worker dalla pagina principale come nell'esempio di codice riportato di seguito. In genere, questa operazione viene eseguita una volta caricata l'app.
window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js");
}
});
Il codice del service worker
A prima vista, i contenuti del file service worker effettivo potrebbero sembrare un po' complessi, ma i
commenti nell'esempio riportato di seguito dovrebbero chiarire la situazione. L'idea di base è di prememorizzare nella cache un file denominato
offline.html
che viene pubblicato solo nelle richieste di navigazione non riuscite e di 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.
});
La pagina di riserva offline
Il file offline.html
è il luogo in cui puoi dare sfogo alla tua creatività, adattarlo alle tue esigenze e aggiungere il tuo
branding. L'esempio seguente mostra il minimo indispensabile di ciò che è possibile fare.
Mostra sia il ricaricamento manuale in base alla pressione di un pulsante sia il ricaricamento automatico
in base all'evento online
e al polling regolare 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 GitHub.
Nota a margine su come rendere installabile la tua app
Ora che il tuo sito ha una pagina di riserva offline, potresti chiederti quali sono i passaggi successivi. Per rendere installabile la tua app, devi aggiungere un manifest dell'app web e, facoltativamente, ideare una strategia di installazione.
Nota a margine sulla pubblicazione di una pagina di riserva offline con Workbox.js
Potresti aver sentito parlare di Workbox. Workbox è un insieme di librerie JavaScript per aggiungere il supporto offline alle app web. Se preferisci scrivere meno codice del service worker, puoi utilizzare la ricetta di Workbox per una pagina offline.
Successivamente, scopri come definire una strategia di installazione per la tua app.