Con i service worker, abbiamo offerto agli sviluppatori un modo per risolvere i problemi di connessione di rete. Hai il controllo della memorizzazione nella cache e della gestione delle richieste. Ciò significa che puoi creare i tuoi pattern. Dai un'occhiata ad alcuni possibili pattern isolati, ma in pratica probabilmente li utilizzerai in tandem, a seconda dell'URL e del contesto.
Per una demo funzionante di alcuni di questi pattern, vedi Trained-to-thrill.
Quando archiviare le risorse
I service worker ti consentono di gestire le richieste indipendentemente dalla memorizzazione nella cache, quindi li mostrerò separatamente. Innanzitutto, determina quando devi utilizzare la cache.
Durante l'installazione, come dipendenza
L'API Service Worker ti offre un evento install. Puoi utilizzarlo per preparare
gli elementi che devono essere pronti prima di gestire altri eventi. Durante
install, le versioni precedenti del service worker continuano a essere eseguite e a pubblicare
le pagine. Qualsiasi azione tu intraprenda in questo momento non deve interrompere il service worker esistente.
Ideale per: CSS, immagini, caratteri, JS, modelli o qualsiasi altro elemento che consideri statico per quella versione del tuo sito.
Recupera gli elementi che renderebbero il tuo sito completamente non funzionante se non venissero recuperati, elementi che un'app specifica per la piattaforma equivalente renderebbe parte del download iniziale.
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('mysite-static-v3').then(function (cache) {
return cache.addAll([
'/css/whatever-v3.css',
'/css/imgs/sprites-v6.png',
'/css/fonts/whatever-v8.woff',
'/js/all-min-v4.js',
// etc.
]);
}),
);
});
event.waitUntil accetta una promessa per definire la durata e il successo dell'installazione. Se la promessa
viene rifiutata, l'installazione viene considerata un errore e questo service worker viene abbandonato (se è in esecuzione una
versione precedente, questa rimarrà intatta). caches.open() e cache.addAll() promesse di restituzione.
Se il recupero di una delle risorse non va a buon fine, la chiamata cache.addAll() viene rifiutata.
Su training-to-thrill lo uso per memorizzare nella cache le risorse statiche.
All'installazione, non come dipendenza
È simile all'installazione come dipendenza, ma non ritarda il completamento dell'installazione e non causa errori di installazione se la memorizzazione nella cache non va a buon fine.
Ideale per: risorse più grandi che non sono necessarie immediatamente, ad esempio asset per i livelli successivi di un gioco.
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open('mygame-core-v1').then(function (cache) {
cache
.addAll
// levels 11-20
();
return cache
.addAll
// core assets and levels 1-10
();
}),
);
});
Questo esempio non supera la promessa cache.addAll per i livelli 11-20 a event.waitUntil, quindi anche se non va a buon fine, il gioco sarà comunque disponibile offline. Naturalmente, dovrai
tenere conto della possibile assenza di questi livelli e tentare di memorizzarli nella cache se
mancano.
Il service worker potrebbe essere interrotto durante il download dei livelli 11-20 perché ha terminato la gestione degli eventi, il che significa che non verranno memorizzati nella cache. L'API Web Periodic Background Synchronization può gestire casi come questo e download più grandi, come i film.
On activate
Ideale per: pulizia e migrazione.
Una volta installato un nuovo service worker e quando non viene utilizzata una versione precedente, il nuovo service worker
si attiva e ricevi un evento activate. Poiché la versione precedente non è più in uso, è un buon
momento per gestire
le migrazioni dello schema in IndexedDB
ed eliminare anche le cache inutilizzate.
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames
.filter(function (cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
})
.map(function (cacheName) {
return caches.delete(cacheName);
}),
);
}),
);
});
Durante l'attivazione, gli eventi come fetch vengono inseriti in una coda, pertanto un'attivazione lunga potrebbe
bloccare i caricamenti di pagina. Mantieni l'attivazione il più semplice possibile e utilizzala solo per le cose che
non potevi fare mentre la versione precedente era attiva.
Su training-to-thrill lo uso per rimuovere le vecchie cache.
All'interazione dell'utente
Ideale per: quando non è possibile rendere disponibile offline l'intero sito e hai scelto di consentire all'utente di selezionare i contenuti che vuole rendere disponibili offline. Ad esempio, un video su YouTube, un articolo su Wikipedia, una galleria specifica su Flickr.
Fornisci all'utente un pulsante "Leggi in un secondo momento" o "Salva per la visione offline". Quando viene fatto clic, recupera ciò che ti serve dalla rete e inseriscilo nella cache.
document.querySelector('.cache-article').addEventListener('click', function (event) {
event.preventDefault();
var id = this.dataset.articleId;
caches.open('mysite-article-' + id).then(function (cache) {
fetch('/get-article-urls?id=' + id)
.then(function (response) {
// /get-article-urls returns a JSON-encoded array of
// resource URLs that a given article depends on
return response.json();
})
.then(function (urls) {
cache.addAll(urls);
});
});
});
L'API Cache è disponibile da pagine e service worker, il che significa che puoi aggiungere elementi alla cache direttamente dalla pagina.
Sulla risposta della rete
Ideale per: aggiornare frequentemente risorse come la casella di posta di un utente o i contenuti di un articolo. Utile anche per contenuti non essenziali come gli avatar, ma è necessario prestare attenzione.
Se una richiesta non corrisponde a nulla nella cache, recuperala dalla rete, inviala alla pagina e aggiungila alla cache contemporaneamente.
Se lo fai per una serie di URL, ad esempio gli avatar, devi fare attenzione a non gonfiare lo spazio di archiviazione dell'origine. Se l'utente deve recuperare spazio su disco, non vuoi essere il candidato principale. Assicurati di eliminare gli elementi nella cache che non ti servono più.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
return (
response ||
fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
})
);
});
}),
);
});
Per consentire un utilizzo efficiente della memoria, puoi leggere il corpo di una risposta/richiesta una sola volta. L'esempio di codice
utilizza .clone() per creare copie
aggiuntive che possono essere lette separatamente.
Su training-to-thrill lo utilizzo per memorizzare nella cache le immagini di Flickr.
Stale-while-revalidate
Ideale per: risorse aggiornate di frequente in cui non è essenziale avere l'ultima versione. Gli avatar possono rientrare in questa categoria.
Se è disponibile una versione memorizzata nella cache, utilizzala, ma recupera un aggiornamento per la prossima volta.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
var fetchPromise = fetch(event.request).then(function (networkResponse) {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return response || fetchPromise;
});
}),
);
});
Questo processo è molto simile a stale-while-revalidate di HTTP.
Sul messaggio push
L'API Push è un'altra funzionalità basata sul service worker. In questo modo, il service worker può essere riattivato in risposta a un messaggio del servizio di messaggistica del sistema operativo. Ciò si verifica anche quando l'utente non ha una scheda aperta sul tuo sito. Viene attivato solo il service worker. Richiedi l'autorizzazione per farlo da una pagina e all'utente viene mostrato un prompt.
Ideale per: contenuti relativi a una notifica, ad esempio un messaggio di chat, una notizia dell'ultima ora o un'email. Inoltre, contenuti che cambiano di rado e che traggono vantaggio dalla sincronizzazione immediata, ad esempio un aggiornamento di una lista di cose da fare o una modifica del calendario.
Il risultato finale comune è una notifica che, quando viene toccata, apre e mette in evidenza una pagina pertinente e per la quale l'aggiornamento delle cache in anticipo è estremamente importante. L'utente è online al momento della ricezione del messaggio push, ma potrebbe non esserlo quando interagisce con la notifica, quindi è fondamentale rendere disponibili questi contenuti offline.
Questo codice aggiorna le cache prima di mostrare una notifica:
self.addEventListener('push', function (event) {
if (event.data.text() == 'new-email') {
event.waitUntil(
caches
.open('mysite-dynamic')
.then(function (cache) {
return fetch('/inbox.json').then(function (response) {
cache.put('/inbox.json', response.clone());
return response.json();
});
})
.then(function (emails) {
registration.showNotification('New email', {
body: 'From ' + emails[0].from.name,
tag: 'new-email',
});
}),
);
}
});
self.addEventListener('notificationclick', function (event) {
if (event.notification.tag == 'new-email') {
// Assume that all of the resources needed to render
// /inbox/ have previously been cached, e.g. as part
// of the install handler.
new WindowClient('/inbox/');
}
});
On background-sync
La sincronizzazione in background è un'altra funzionalità basata sul service worker. Consente di richiedere la sincronizzazione dei dati in background una sola volta o a un intervallo (estremamente euristico). Ciò si verifica anche quando l'utente non ha aperto una scheda del tuo sito. Viene riattivato solo il service worker. Richiedi l'autorizzazione per farlo da una pagina e l'utente riceve una richiesta.
Ideale per: aggiornamenti non urgenti, in particolare quelli che si verificano così regolarmente che un messaggio push per aggiornamento sarebbe troppo frequente per gli utenti, come le cronologie dei social o gli articoli di notizie.
self.addEventListener('sync', function (event) {
if (event.id == 'update-leaderboard') {
event.waitUntil(
caches.open('mygame-dynamic').then(function (cache) {
return cache.add('/leaderboard.json');
}),
);
}
});
Persistenza della cache
Alla tua origine viene assegnata una certa quantità di spazio libero da utilizzare a suo piacimento. Lo spazio libero è condiviso tra tutti gli spazi di archiviazione di origine: Archiviazione(locale), IndexedDB, Accesso al file system e, naturalmente, Cache.
L'importo che ricevi non è specificato. Varia a seconda del dispositivo e delle condizioni di archiviazione. Puoi scoprire il tuo saldo con:
if (navigator.storage && navigator.storage.estimate) {
const quota = await navigator.storage.estimate();
// quota.usage -> Number of bytes used.
// quota.quota -> Maximum number of bytes available.
const percentageUsed = (quota.usage / quota.quota) * 100;
console.log(`You've used ${percentageUsed}% of the available storage.`);
const remaining = quota.quota - quota.usage;
console.log(`You can write up to ${remaining} more bytes.`);
}
Tuttavia, come per tutta la memoria del browser, il browser è libero di eliminare i dati se il dispositivo è sotto pressione di archiviazione. Purtroppo il browser non riesce a distinguere tra i film che vuoi conservare a tutti i costi e il gioco che non ti interessa molto.
Per risolvere il problema, utilizza l'interfaccia StorageManager:
// From a page:
navigator.storage.persist()
.then(function(persisted) {
if (persisted) {
// Hurrah, your data is here to stay!
} else {
// So sad, your data may get chucked. Sorry.
});
Naturalmente, l'utente deve concedere l'autorizzazione. Per farlo, utilizza l'API Permissions.
È importante che l'utente faccia parte di questo flusso, in quanto ora possiamo aspettarci che abbia il controllo dell'eliminazione. Se il dispositivo è sotto pressione per lo spazio di archiviazione e l'eliminazione dei dati non essenziali non risolve il problema, l'utente può decidere quali elementi conservare e rimuovere.
Affinché funzioni, i sistemi operativi devono trattare le origini "durabili" come equivalenti alle app specifiche della piattaforma nelle loro suddivisioni dell'utilizzo dello spazio di archiviazione, anziché segnalare il browser come un singolo elemento.
Visualizzazione dei suggerimenti
Non importa quanta memorizzazione nella cache esegui, il service worker utilizza la cache solo quando gli indichi quando e come. Ecco alcuni pattern per gestire le richieste:
Solo cache
Ideale per: qualsiasi elemento che consideri statico per una determinata "versione" del tuo sito. Dovresti averli memorizzati nella cache nell'evento di installazione, quindi puoi fare affidamento sulla loro presenza.
self.addEventListener('fetch', function (event) {
// If a match isn't found in the cache, the response
// will look like a connection error
event.respondWith(caches.match(event.request));
});
…anche se non è necessario gestire questo caso in modo specifico, Cache, fallback alla rete lo copre.
Solo rete
Ideale per: elementi che non hanno un equivalente offline, come i ping di analisi e le richieste non GET.
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or don't call event.respondWith, which
// will result in default browser behavior
});
… anche se non è necessario gestire spesso questo caso specifico, la cache, con fallback alla rete lo copre.
Cache, fallback alla rete
Ideale per: creare app offline-first. In questi casi, gestirai la maggior parte delle richieste in questo modo. Altri pattern sono eccezioni basate sulla richiesta in entrata.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
In questo modo, ottieni il comportamento "solo cache" per gli elementi nella cache e il comportamento "solo rete" per qualsiasi elemento non memorizzato nella cache (incluse tutte le richieste non GET, in quanto non possono essere memorizzate nella cache).
Cache e race di rete
Ideale per: asset di piccole dimensioni in cui si cerca di migliorare le prestazioni sui dispositivi con accesso lento al disco.
Con alcune combinazioni di dischi rigidi meno recenti, scanner antivirus e connessioni a internet più veloci, recuperare risorse dalla rete può essere più rapido che accedere al disco. Tuttavia, accedere alla rete quando l'utente ha i contenuti sul proprio dispositivo può comportare un consumo eccessivo di dati, quindi tienilo presente.
// Promise.race rejects when a promise rejects before fulfilling.
// To make a race function:
function promiseAny(promises) {
return new Promise((resolve, reject) => {
// make sure promises are all promises
promises = promises.map((p) => Promise.resolve(p));
// resolve this promise as soon as one resolves
promises.forEach((p) => p.then(resolve));
// reject if all promises reject
promises.reduce((a, b) => a.catch(() => b)).catch(() => reject(Error('All failed')));
});
}
self.addEventListener('fetch', function (event) {
event.respondWith(promiseAny([caches.match(event.request), fetch(event.request)]));
});
La rete torna alla cache
Ideale per: una soluzione rapida per le risorse che vengono aggiornate di frequente, al di fuori della "versione" del sito. Ad esempio, articoli, avatar, timeline dei social media e classifiche dei giochi.
Ciò significa che gli utenti online ricevono i contenuti più aggiornati, mentre gli utenti offline ricevono una versione precedente memorizzata nella cache. Se la richiesta di rete va a buon fine, molto probabilmente vorrai aggiornare la voce della cache.
Tuttavia, questo metodo presenta dei difetti. Se l'utente ha una connessione intermittente o lenta, dovrà attendere che la rete non funzioni prima di ricevere i contenuti perfettamente accettabili già presenti sul suo dispositivo. Questa operazione può richiedere molto tempo e l'esperienza utente è frustrante. Per una soluzione migliore, consulta il pattern successivo, Cache, poi rete.
self.addEventListener('fetch', function (event) {
event.respondWith(
fetch(event.request).catch(function () {
return caches.match(event.request);
}),
);
});
Cache, poi rete
Ideale per: contenuti che vengono aggiornati di frequente. Ad esempio, articoli, timeline dei social media e classifiche dei giochi.
Per farlo, la pagina deve effettuare due richieste, una alla cache e una alla rete. L'idea è di mostrare prima i dati memorizzati nella cache, quindi aggiornare la pagina quando e se arrivano i dati di rete.
A volte puoi semplicemente sostituire i dati attuali quando arrivano nuovi dati (ad esempio una classifica di gioco), ma questo può essere problematico con contenuti più grandi. In sostanza, non "far sparire " qualcosa che l'utente potrebbe leggere o con cui interagire.
Twitter aggiunge i nuovi contenuti sopra quelli vecchi e regola la posizione di scorrimento in modo che l'utente non venga interrotto. Ciò è possibile perché Twitter mantiene un ordine dei contenuti per lo più lineare. Ho copiato questo pattern per addestrato per emozionare per visualizzare i contenuti sullo schermo il più rapidamente possibile, mentre mostro i contenuti aggiornati non appena arrivano.
Codice nella pagina:
var networkDataReceived = false;
startSpinner();
// fetch fresh data
var networkUpdate = fetch('/data.json')
.then(function (response) {
return response.json();
})
.then(function (data) {
networkDataReceived = true;
updatePage(data);
});
// fetch cached data
caches
.match('/data.json')
.then(function (response) {
if (!response) throw Error('No data');
return response.json();
})
.then(function (data) {
// don't overwrite newer network data
if (!networkDataReceived) {
updatePage(data);
}
})
.catch(function () {
// we didn't get cached data, the network is our last hope:
return networkUpdate;
})
.catch(showErrorMessage)
.then(stopSpinner);
Codice nel service worker:
Devi sempre accedere alla rete e aggiornare una cache man mano che procedi.
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
}),
);
});
In training-to-thrill ho aggirato il problema utilizzando XHR anziché fetch e sfruttando l'intestazione Accept per indicare al service worker da dove recuperare il risultato (codice pagina, codice service worker).
Fallback generico
Se non riesci a pubblicare contenuti dalla cache o dalla rete, fornisci un fallback generico.
Ideale per: immagini secondarie come avatar, richieste POST non riuscite e una pagina "Non disponibile offline".
self.addEventListener('fetch', function (event) {
event.respondWith(
// Try the cache
caches
.match(event.request)
.then(function (response) {
// Fall back to network
return response || fetch(event.request);
})
.catch(function () {
// If both fail, show a generic fallback:
return caches.match('/offline.html');
// However, in reality you'd have many different
// fallbacks, depending on URL and headers.
// Eg, a fallback silhouette image for avatars.
}),
);
});
L'elemento a cui esegui il fallback è probabilmente una dipendenza di installazione.
Se la pagina pubblica un'email, il service worker potrebbe ricorrere all'archiviazione dell'email in una posta in uscita IndexedDB e rispondere comunicando alla pagina che l'invio non è riuscito, ma i dati sono stati conservati correttamente.
Modelli lato service worker
Ideale per: pagine per le quali non è possibile memorizzare nella cache la risposta del server.
Il rendering delle pagine sul server è più veloce, ma ciò può significare includere dati di stato che potrebbero non avere senso in una cache, ad esempio lo stato di accesso. Se la pagina è controllata da un service worker, puoi scegliere di richiedere dati JSON insieme a un modello e di eseguire il rendering di questi dati.
importScripts('templating-engine.js');
self.addEventListener('fetch', function (event) {
var requestURL = new URL(event.request.url);
event.respondWith(
Promise.all([
caches.match('/article-template.html').then(function (response) {
return response.text();
}),
caches.match(requestURL.path + '.json').then(function (response) {
return response.json();
}),
]).then(function (responses) {
var template = responses[0];
var data = responses[1];
return new Response(renderTemplate(template, data), {
headers: {
'Content-Type': 'text/html',
},
});
}),
);
});
Mettere insieme i pezzi
Non sei limitato a uno di questi metodi. Infatti, probabilmente ne utilizzerai molti a seconda dell'URL della richiesta. Ad esempio, addestrato per emozionare utilizza:
- Cache on install, per l'interfaccia utente e il comportamento statici
- Cache sulla risposta di rete, per le immagini e i dati di Flickr
- Recupero dalla cache, con fallback alla rete, per la maggior parte delle richieste
- Recupera dalla cache, poi dalla rete, per i risultati di ricerca di Flickr
Ti basta esaminare la richiesta e decidere cosa fare:
self.addEventListener('fetch', function (event) {
// Parse the URL:
var requestURL = new URL(event.request.url);
// Handle requests to a particular host specifically
if (requestURL.hostname == 'api.example.com') {
event.respondWith(/* some combination of patterns */);
return;
}
// Routing for local URLs
if (requestURL.origin == location.origin) {
// Handle article URLs
if (/^\/article\//.test(requestURL.pathname)) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (/\.webp$/.test(requestURL.pathname)) {
event.respondWith(/* some other combination of patterns */);
return;
}
if (request.method == 'POST') {
event.respondWith(/* some other combination of patterns */);
return;
}
if (/cheese/.test(requestURL.pathname)) {
event.respondWith(
new Response('Flagrant cheese error', {
status: 512,
}),
);
return;
}
}
// A sensible default pattern
event.respondWith(
caches.match(event.request).then(function (response) {
return response || fetch(event.request);
}),
);
});
Per approfondire
- Service worker e API Cache Storage
- JavaScript Promises—an Introduction: guida alle promesse
Crediti
Per le icone adorabili:
- Code di buzzyrobot
- Calendar di Scott Lewis
- Network di Ben Rizzo
- SD di Thomas Le Bas
- CPU di iconsmind.com
- Trash di trasnik
- Notifica di @daosme
- Layout di Mister Pixel
- Cloud di P.J. Onori
Grazie a Jeff Posnick per aver individuato molti errori ululanti prima che premessi "Pubblica".