Con Service Worker abbiamo rinunciato a risolvere problemi offline e abbiamo dato agli sviluppatori le parti mobili per risolverli autonomamente. Ti offre il controllo sulla memorizzazione nella cache e sulla gestione delle richieste. Ciò significa che puoi creare i tuoi modelli. Diamo un'occhiata ad alcuni possibili pattern isolati, ma in pratica probabilmente ne utilizzerai molti in tandem, a seconda dell'URL e del contesto.
Per una demo funzionante di alcuni di questi pattern, guarda Addestrato al brivido e questo video che mostra l'impatto del rendimento.
La macchina cache: quando archiviare le risorse
Service Worker consente di gestire le richieste in modo indipendente dalla memorizzazione nella cache, quindi le mostrerò separatamente. Innanzitutto, la memorizzazione nella cache, quando deve essere eseguita?
Al momento dell'installazione, come dipendenza
Il service worker ti fornisce un evento install
. Puoi usare questa funzionalità per preparare cose
che devono essere pronte prima di gestire altri eventi. Anche se ciò accade qualsiasi versione precedente di Service Worker è ancora in esecuzione e pubblica pagine, le operazioni che esegui qui non devono interrompere la situazione.
Ideale per: CSS, immagini, caratteri, JS, modelli... praticamente tutto ciò che consideri statico in quella "versione" del sito.
Si tratta di elementi che renderebbero il tuo sito completamente non funzionale se non venissero recuperati, ovvero gli elementi che un'app equivalente specifica per la piattaforma fa 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
si impegna a definire la durata e il successo dell'installazione. Se la promessa viene rifiutata, l'installazione viene considerata non riuscita e questo service worker verrà abbandonato (se è in esecuzione una versione precedente, verrà lasciata intatta). caches.open()
e cache.addAll()
hanno promesso di reso.
Se il recupero di una qualsiasi delle risorse non riesce, la chiamata cache.addAll()
viene rifiutata.
Nell'addestrato per thrill lo uso per memorizzare nella cache gli asset statici.
Al momento dell'installazione, non come dipendenza
Si tratta di una procedura simile a quella riportata sopra, ma non ritarda il completamento dell'installazione e non determina la mancata riuscita dell'installazione se la memorizzazione nella cache non va a buon fine.
Ideale per: risorse più grandi che non sono immediatamente necessarie, 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
();
}),
);
});
L'esempio riportato sopra non rispetta la promessa cache.addAll
per i livelli 11-20 a
event.waitUntil
, quindi, anche se non funziona, il gioco sarà comunque disponibile offline. Naturalmente, dovrai
soddisfare la possibile assenza di questi livelli e tentare nuovamente di memorizzarli nella cache se
mancano.
Il service worker può essere interrotto durante il download dei livelli 11-20, poiché ha terminato di gestire gli eventi, il che significa che non verranno memorizzati nella cache. In futuro, l'API Web Periodic Background Synchronization gestirà casi come questo e download di dimensioni maggiori come i filmati. Quell'API è attualmente supportata solo sulle fork di Chromium.
All'attivazione
Ideale per:pulizia e migrazione.
Una volta installato un nuovo Service Worker e una versione precedente non viene utilizzata, la nuova versione viene attivata e viene generato un evento activate
. Poiché la versione precedente è obsoleta, è un buon momento per gestire le migrazioni di schemi in IndexedDB ed eliminare 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, altri eventi, come fetch
, vengono messi in coda, quindi un'attivazione lunga potrebbe
bloccare potenzialmente i caricamenti delle pagine. Mantieni la tua attivazione il più snella possibile e utilizzala solo per cose che
non potevi fare mentre era attiva la versione precedente.
Nell'addestrato per thrill lo uso per rimuovere le vecchie cache.
Al momento dell'interazione dell'utente
Ideale per: quando l'intero sito non può essere messo offline e hai scelto di consentire all'utente di selezionare i contenuti che desidera rendere disponibili offline. un video su qualcosa del tipo YouTube, un articolo su Wikipedia, una galleria specifica su Flickr.
Assegna all'utente un pulsante "Leggi più tardi" o "Salva per offline". Dopo il 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 sia dalle pagine sia dai service worker, il che significa che puoi aggiungerla alla cache direttamente dalla pagina.
Alla risposta di rete
Ideale per: aggiornare frequentemente risorse come la posta in arrivo di un utente o i contenuti degli articoli. È utile anche per i contenuti non essenziali come gli avatar, ma è necessaria la cura.
Se una richiesta non corrisponde ad alcun elemento nella cache, recuperala dalla rete, inviala alla pagina e, contemporaneamente, aggiungila alla cache.
Se esegui questa operazione per una serie di URL, ad esempio gli avatar, dovrai fare attenzione a non esaurire lo spazio di archiviazione dell'origine. Se l'utente ha bisogno di liberare spazio su disco, non vuoi essere il candidato principale. Assicurati di eliminare gli elementi contenuti 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. Il codice riportato sopra utilizza .clone()
per creare copie aggiuntive che possono essere lette separatamente.
Nel campo training-to-thrill lo uso per memorizzare nella cache le immagini di Flickr.
Riconvalida in caso di inattività
Ideale per: aggiornare spesso risorse in cui avere la versione più recente non è essenziale. 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;
});
}),
);
});
È molto simile a stale-while-revalidate di HTTP.
Al momento del messaggio push
L'API Push è un'altra funzionalità basata su Service Worker. Ciò consente al service worker di essere riattivato in risposta a un messaggio del servizio di messaggistica del sistema operativo. Questo accade anche quando l'utente non ha una scheda aperta sul tuo sito. Solo il service Worker viene svegliato. Chiedi l'autorizzazione a eseguire questa operazione da una pagina e all'utente viene chiesto.
Ideale per: contenuti correlati a una notifica, ad esempio un messaggio di chat, una notizia dell'ultima ora o un'email. Inoltre, vengono modificati raramente contenuti che sfruttano la sincronizzazione immediata, come l'aggiornamento di un elenco di cose da fare o l'alterazione di un calendario.
Il risultato finale comune è una notifica che, quando viene toccata, apre/imposta una pagina pertinente, ma per la quale l'aggiornamento delle cache prima che ciò accada è extremely importante. L'utente è ovviamente online quando riceve il messaggio push, ma potrebbe non esserlo quando interagisce con la notifica, per cui è importante rendere questi contenuti disponibili 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/');
}
});
Attiva la sincronizzazione in background
La sincronizzazione in background è un'altra funzionalità basata su Service Worker. Consente di richiedere la sincronizzazione dei dati in background come una tantum o in base a un intervallo (estremamente euristico). Questo accade anche quando l'utente non ha una scheda aperta sul tuo sito. Solo il service worker è stato svegliato. Richiedi l'autorizzazione per eseguire questa operazione da una pagina e all'utente verrà chiesto.
Ideale per: aggiornamenti non urgenti, in particolare quelli che si verificano talmente regolarmente che un messaggio push di ogni aggiornamento sarebbe troppo frequente per gli utenti, ad esempio cronologie social o articoli.
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 data una certa quantità di spazio libero per fare ciò che vuole. Questo spazio libero viene condiviso tra tutti gli archivi di origine: Storage(locale), IndexedDB, Accesso al file system e, ovviamente, le cache.
L'importo ottenuto non è specificato. Varia in base alle condizioni del dispositivo e dello spazio di archiviazione. Puoi scoprire quanto hai guadagnato tramite:
navigator.storageQuota.queryInfo('temporary').then(function (info) {
console.log(info.quota);
// Result: <quota in bytes>
console.log(info.usage);
// Result: <used data in bytes>
});
Tuttavia, come per lo spazio di archiviazione del browser, il browser è libero di eliminare i tuoi dati se il dispositivo è sottoposto a pressione di archiviazione. Sfortunatamente, il browser non è in grado di distinguere a tutti i costi i film che vuoi conservare e il gioco che non ti interessa.
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.
Rendere l'utente parte di questa procedura è importante, poiché ora possiamo aspettarci che abbia il controllo dell'eliminazione. Se il dispositivo è sottoposto a pressione di archiviazione e la cancellazione dei dati non essenziali non risolve il problema, l'utente può decidere quali elementi conservare e rimuovere.
Affinché funzioni, richiede che i sistemi operativi trattino le origini "durevoli" come equivalenti alle app specifiche della piattaforma nelle suddivisioni dell'utilizzo dello spazio di archiviazione, anziché segnalare il browser come un singolo elemento.
Pubblicazione dei suggerimenti: risposta alle richieste
Indipendentemente dalla quantità di memorizzazione nella cache, il service worker non utilizzerà la cache a meno che tu non gli comunichi quando e come. Ecco alcuni pattern per la gestione delle richieste:
Solo cache
Ideale per:tutto ciò che consideri statico per una determinata "versione" del tuo sito. Dovresti averli memorizzati nella cache durante l'evento di installazione, così puoi essere certo che siano presenti.
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 spesso non devi gestire in modo specifico questo caso, Cache, ripiegando sulla rete lo copre.
Solo rete
Ideale per: cose che non hanno equivalenti offline, come ping di analisi e richieste non GET.
self.addEventListener('fetch', function (event) {
event.respondWith(fetch(event.request));
// or simply don't call event.respondWith, which
// will result in default browser behavior
});
...anche se spesso non devi gestire in modo specifico questo caso, Cache, ripiegando sulla rete lo copre.
Cache, passaggio alla rete
Ideale per: creare soluzioni offline. In questi casi, è la modalità di gestione della maggior parte delle richieste. Altri pattern saranno eccezioni in base alla richiesta in arrivo.
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 contenuti nella cache e il comportamento "solo rete" per tutti gli elementi non memorizzati nella cache (incluse tutte le richieste non GET, poiché non possono essere memorizzate nella cache).
Gara di cache e rete
Ideale per: asset di piccole dimensioni per cui stai migliorando prestazioni sui dispositivi con accesso lento al disco.
Con alcune combinazioni di dischi rigidi meno recenti, scanner antivirus e connessioni a internet più veloci, ottenere le risorse dalla rete può essere più veloce che andare su disco. Tuttavia, visitare la rete quando l'utente ha i contenuti sul dispositivo può essere uno spreco di dati, tieni presente questo aspetto.
// Promise.race is no good to us because it rejects if
// a promise rejects before fulfilling. Let's make a proper
// 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)]));
});
Errore di rete nella cache
Ideale per: una soluzione rapida per risorse che vengono aggiornate di frequente al di fuori della "versione" del sito. Ad esempio, articoli, avatar, tempistiche dei social media e classifiche dei giochi.
In questo modo, offri agli utenti online i contenuti più aggiornati, mentre gli utenti offline ricevono una versione precedente memorizzata nella cache. Se la richiesta di rete ha esito positivo, molto probabilmente vorrai aggiornare la voce della cache.
Tuttavia, questo metodo presenta dei problemi. Se l'utente ha una connessione lenta o intermittente, dovrà attendere che la rete non riesca prima di poter ricevere i contenuti perfettamente accettabili già sul suo dispositivo. Questa operazione può richiedere molto tempo ed è un'esperienza utente frustrante. Per una soluzione migliore, vedi il pattern successivo, Cache, quindi 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, cronologia dei social media e giochi. classifiche.
Ciò richiede che la pagina effettui due richieste, una alla cache e una alla rete. L'idea è mostrare prima i dati memorizzati nella cache, quindi aggiornare la pagina quando e se arrivano i dati di rete.
A volte è possibile semplicemente sostituire i dati attuali quando arrivano nuovi dati (ad es. la classifica del gioco), ma ciò può creare interruzioni con contenuti più grandi. Fondamentalmente, non "nascondere " qualcosa che l'utente sta leggendo o con cui potrebbe interagire.
Twitter aggiunge i nuovi contenuti sopra quelli precedenti e regola la posizione di scorrimento in modo che l'utente rimanga senza interruzioni. Ciò è possibile perché Twitter mantiene per lo più un ordine prevalentemente lineare per i contenuti. Ho copiato questo pattern per addestrato per il brivido affinché i contenuti vengano visualizzati sullo schermo il più rapidamente possibile, mostrando al contempo 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:
Dovresti sempre accedere alla rete e aggiornare la 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;
});
}),
);
});
Nella fase addestrato per il thrill, ho risolto questo problema utilizzando XHR anziché fetch, e utilizzando in modo illecito l'intestazione Accept per indicare al service worker da dove ottenere il risultato (codice pagina, codice del service worker).
Di riserva generico
Se non riesci a fornire qualcosa dalla cache e/o dalla rete, puoi fornire un modello di riserva generico.
Ideale per: immagini secondarie come avatar, richieste POST non riuscite e una pagina "Non disponibile in modalità 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.
}),
);
});
È probabile che l'elemento di riserva a cui esegui l'operazione sia una dipendenza di installazione.
Se la tua pagina pubblica un'email, il service worker potrebbe archiviare l'email in una "Posta in uscita" di IndexedDB e rispondere comunicando alla pagina che l'invio non è riuscito, ma i dati sono stati conservati correttamente.
Modelli lato worker
Ideale per: pagine per le quali la risposta del server non può essere memorizzata nella cache.
Il rendering delle pagine sul server velocizza le cose, ma questo può significare includere dati di stato che potrebbero non avere senso in una cache, ad esempio "Accesso eseguito come...". Se la pagina è controllata da un service worker, puoi scegliere di richiedere dati JSON insieme a un modello ed eseguire il rendering.
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',
},
});
}),
);
});
Elaborazione
Non devi limitarti a uno di questi metodi. Anzi, probabilmente ne utilizzerai molti, a seconda dell'URL della richiesta. Ad esempio, la parola addestrato per entusiasmare utilizza:
- cache al momento dell'installazione, per l'interfaccia utente statica e il comportamento
- cache on network response per dati e immagini di Flickr
- recuperare dalla cache, tornare alla rete, per la maggior parte delle richieste
- recupera dalla cache e quindi dalla rete per i risultati di ricerca di Flickr
Esamina la richiesta e decidi 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);
}),
);
});
... hai capito bene.
Crediti
... per le bellissime icone:
- Codice di Livelyrobot
- Calendar di Scott Lewis
- Rete di Ben Rizzo
- SD di Thomas Le Bas
- CPU di iconmind.com
- Cestino di trasnik
- Notifica di @daosme
- Layout di Mister Pixel
- Cloud di P.J. Onori
Grazie a Jeff Posnick per aver individuato molti errori di urlo prima di premere "pubblica".
Per approfondire
- Service worker: introduzione
- Il service worker è pronto?: monitora lo stato dell'implementazione nei browser principali
- JavaScript Promises—an Introduction - guida alle promesse