Offline med Cache API
En service worker som bara finns gör ingen skillnad. Det är när den svarar på förfrågningarna, och har svaren sparade, som applikationen börjar fungera offline. Den här artikeln bygger färdigt service workern för memoryspelet med Cache API, och den bygger på föregående artikel, Service workers.
Tidsåtgång: Ungefär 15 minuter.
Cache API
Section titled “Cache API”Cache API sparar svar på förfrågningar. Det är ett asynkront API, och det finns både i service workern och på
sidan, under namnet caches:
cacheshåller ordning på namngivna cachar.caches.open(name)öppnar en cache och skapar den om den inte finns,caches.keys()ger namnen ochcaches.delete(name)tar bort en.- En cache lagrar par av ett
Requestoch ettResponse, samma objekt som du arbetade med i Nätverksanrop med fetch.cache.put(request, response)sparar ett par,cache.match(request)letar upp svaret ochcache.addAll(urls)hämtar och sparar flera adresser på en gång.
const cache = await caches.open('memory-v1')await cache.addAll(['./', './manifest.webmanifest'])
const response = await cache.match('./')console.log(response.status) // 200cache.match() ger undefined om inget svar finns. Cacharna räknas in i samma kvot som IndexedDB,
den du kan fråga om med navigator.storage.estimate() från
Lagring i webbläsaren.
Fånga förfrågningarna
Section titled “Fånga förfrågningarna”I fetch-händelsen bestämmer service workern hur förfrågan ska besvaras. Den anropar
event.respondWith() med ett svar, eller ett löfte om ett svar:
self.addEventListener('fetch', (event) => { const { request } = event
if (request.method !== 'GET' || new URL(request.url).origin !== self.location.origin) { return }
event.respondWith(networkFirst(request))})Anropar lyssnaren inte respondWith() går förfrågan till nätverket som vanligt, som om service workern
inte fanns. Det utnyttjar villkoret överst:
- Bara
GET. EttPOSTändrar något på servern och ska inte besvaras från en cache.cache.put()vägrar dessutom att spara det. - Bara samma origin. Förfrågningar till andra webbplatser, som ett API, lämnas åt nätverket. Att visa gammal data från ett API när användaren är offline är ett beslut om applikationen, inte något som ska hända av sig självt.
respondWith() måste anropas direkt i lyssnaren, inte efter en await. Därför får den ett löfte från
networkFirst(), i stället för att lyssnaren väntar på svaret själv.
WebSocket-anslutningen i Messages passerar aldrig service workern. Den är inget fetch-anrop, och
den behöver en server i andra änden. Offline kan Messages starta, men inte skicka något.
Network first
Section titled “Network first”Den första strategin frågar nätverket först. Går det bra sparas en kopia av svaret, och går det inte används kopian:
async function networkFirst(request) { const cache = await caches.open(CACHE_NAME)
try { const response = await fetch(request) if (response.ok) { await cache.put(request, response.clone()) } return response } catch (error) { const cached = await cache.match(request) if (cached) { return cached } throw error }}fetch() avvisar sitt löfte bara när det inte går att nå servern alls. Ett svar med status 404 är
ett svar, och därför kontrollerar response.ok att bara lyckade svar sparas. Annars kunde en
tillfällig 404 ersätta en fungerande kopia. Saknas både nätverk och kopia kastas felet vidare, och
sidan får ett nätverksfel, precis som utan service worker.
Fråga: varför clone()?
Section titled “Fråga: varför clone()?”Vad händer om networkFirst() sparar och returnerar samma svar, utan clone()?
await cache.put(request, response)return responseSidan får ett nätverksfel. Kroppen i ett Response är en ström som bara kan läsas en gång, på samma
sätt som du bara kan anropa response.json() en gång. cache.put() läser den för att spara den, och
svaret som sedan returneras har en förbrukad kropp, som respondWith() inte kan använda.
clone() skapar ett andra svar med en egen kopia av kroppen, så att ett kan sparas och ett skickas
till sidan.
Network first ger alltid den senaste versionen när användaren är uppkopplad, och den senaste kopian
när användaren inte är det. Priset är att varje förfrågan går till nätverket först, och på en
uppkoppling som finns men inte fungerar kan det dröja innan fetch() ger upp. Mer om det i
Online, offline och Lie-Fi i kursvecka 16.
Cache first
Section titled “Cache first”Den andra strategin vänder på ordningen: finns en kopia används den, och nätverket frågas bara om den saknas.
async function cacheFirst(request) { const cache = await caches.open(CACHE_NAME) const cached = await cache.match(request) if (cached) { return cached }
const response = await fetch(request) if (response.ok) { await cache.put(request, response.clone()) } return response}Det är snabbare, men ger aldrig en nyare version av en fil som redan finns i cachen. Det passar för
filer vars innehåll aldrig ändras under samma adress, och sådana skapar Vite åt dig. När du kör
npm run build får varje fil som byggs ett namn med en hash av innehållet:
dist/├── index.html├── sw.js├── manifest.webmanifest└── assets/ ├── index-DpQX1ODm.js ├── index-CGINoPm8.css ├── 1-ADx4FO_M.png └── ...Ändrar du koden får index.js ett nytt namn, och index.html pekar på det nya namnet. En fil under
/assets/ ändras alltså aldrig, den ersätts av en fil med ett annat namn. Service workern kan välja
strategi efter adressen:
event.respondWith( new URL(request.url).pathname.startsWith('/assets/') ? cacheFirst(request) : networkFirst(request))Fråga: cache first för allt?
Section titled “Fråga: cache first för allt?”Det går ju snabbare. Du byter till cacheFirst() för alla förfrågningar, också för dokumentet. Applikationen
fungerar fint offline. Sedan rättar du en bugg, bygger och publicerar. Vad ser användarna?
Den gamla versionen, med buggen. Dokumentet, index.html, finns i cachen och används utan att
nätverket frågas. Det pekar på de gamla filerna i /assets/, som också finns i cachen. Den nya
versionen hämtas aldrig.
Det ändras först om sw.js ändras, så att en ny version installeras och rensar cachen. Dokumentet,
och andra filer vars namn inte ändras, behöver network first, så att användaren får den senaste
versionen när nätverket finns.
Vad ska cachas i förväg?
Section titled “Vad ska cachas i förväg?”Med network first och cache first sparas en fil första gången den hämtas genom service workern. Det
kallas att cacha under körning. Det går också att cacha i förväg, i install, så att filerna finns
redan innan någon har bett om dem:
const PRECACHE_URLS = ['./', './manifest.webmanifest', './icons/icon-192.png', './icons/icon-512.png']
self.addEventListener('install', (event) => { event.waitUntil(precache())})
async function precache() { const cache = await caches.open(CACHE_NAME) await cache.addAll(PRECACHE_URLS)}Listan innehåller bara filer från public/ och dokumentet, vars namn är kända. Filerna under
/assets/ kan inte stå där. Deras namn bestäms av Vite vid varje bygge, och sw.js kopieras från
public/ utan att Vite ändrar i den.
Det får en följd som är värd att känna till. Vid första besöket styr service workern inte sidan, som du såg i föregående artikel. Skripten, stilmallen och bilderna hämtas förbi den och hamnar inte i cachen. Applikationen fungerar alltså offline från och med andra besöket, när allt hämtas genom service workern.
För B3 räcker det. Kravet för högre betyg är att applikationen fungerar offline, inklusive
underapplikationer som har öppnats minst en gång medan användaren var uppkopplad, och det är precis
vad cachning under körning ger. Vill du att första besöket ska räcka behöver service workern veta
byggets filnamn. Det är vad verktyg som Workbox, och insticksprogrammet vite-plugin-pwa som bygger
på det, gör: de skriver listan i service workern när du bygger.
Städa gamla cachar
Section titled “Städa gamla cachar”Cachens namn, memory-v1, har två delar: ett prefix för applikationen och en version. Ändrar du hur
service workern cachar, till exempel vad som cachas i förväg, byter du version. Den nya versionen av
service workern skapar då en ny cache, och tar bort de gamla när den aktiveras:
const CACHE_PREFIX = 'memory-'const CACHE_NAME = `${CACHE_PREFIX}v2`
self.addEventListener('activate', (event) => { event.waitUntil(deleteOldCaches())})
async function deleteOldCaches() { const names = await caches.keys() const oldNames = names.filter((name) => name.startsWith(CACHE_PREFIX) && name !== CACHE_NAME) await Promise.all(oldNames.map((name) => caches.delete(name)))}Prefixet är till för samma sak som prefixet på nycklarna i Web Storage: cacharna delas av allt på samma origin. En service worker som tar bort alla cachar utom sin egen kan ta bort en annan applikations.
Byt bara version när du behöver. Med network first uppdateras kopiorna ändå varje gång nätverket finns, och en ny version börjar med en cache som bara innehåller det som cachas i förväg. Resten fylls på under körning.
Hela service workern
Section titled “Hela service workern”const CACHE_PREFIX = 'memory-'const CACHE_NAME = `${CACHE_PREFIX}v1`const PRECACHE_URLS = ['./', './manifest.webmanifest', './icons/icon-192.png', './icons/icon-512.png']
self.addEventListener('install', (event) => { event.waitUntil(precache())})
self.addEventListener('activate', (event) => { event.waitUntil(deleteOldCaches())})
self.addEventListener('fetch', (event) => { const { request } = event const url = new URL(request.url)
if (request.method !== 'GET' || url.origin !== self.location.origin) { return }
event.respondWith(url.pathname.startsWith('/assets/') ? cacheFirst(request) : networkFirst(request))})
/** * Caches the files whose names are known before the application has run. * * @returns {Promise<void>} Fulfilled when every file is cached. */async function precache() { const cache = await caches.open(CACHE_NAME) await cache.addAll(PRECACHE_URLS)}
/** * Deletes this application's caches from earlier versions. * * @returns {Promise<void>} Fulfilled when the old caches are deleted. */async function deleteOldCaches() { const names = await caches.keys() const oldNames = names.filter((name) => name.startsWith(CACHE_PREFIX) && name !== CACHE_NAME) await Promise.all(oldNames.map((name) => caches.delete(name)))}
/** * Answers from the network and keeps a copy, or from the copy when offline. * * @param {Request} request - The request to answer. * @returns {Promise<Response>} The response. */async function networkFirst(request) { const cache = await caches.open(CACHE_NAME)
try { const response = await fetch(request) if (response.ok) { await cache.put(request, response.clone()) } return response } catch (error) { const cached = await cache.match(request) if (cached) { return cached } throw error }}
/** * Answers from the cache, and from the network only when nothing is cached. * * @param {Request} request - The request to answer. * @returns {Promise<Response>} The response. */async function cacheFirst(request) { const cache = await caches.open(CACHE_NAME) const cached = await cache.match(request) if (cached) { return cached }
const response = await fetch(request) if (response.ok) { await cache.put(request, response.clone()) } return response}Funktionerna kan anropas i lyssnarna ovanför sina deklarationer, eftersom funktionsdeklarationer lyfts. Det är först när en händelse kommer som de körs.
Testa offline
Section titled “Testa offline”- Kör
npm run buildochnpm run serve, och öppnahttp://localhost:4173i Chrome eller Edge. - Ladda om sidan en gång, så att allt hämtas genom service workern.
- Kontrollera under Application → Cache storage att
memory-v1innehåller dokumentet och filerna under/assets/. - Kryssa i Offline under Application → Service workers, eller välj Offline i fliken Network, och ladda om. Spelet ska starta och gå att spela, med alla bilder.
Bilderna i memoryspelet importeras från koden och hamnar under /assets/, så de cachas när de visas
första gången. I B3 betyder det att en underapplikation fungerar offline när den har öppnats en gång,
som kravet säger. En bild som bara hämtas när något särskilt händer i spelet finns inte i cachen
förrän det har hänt.
Att ta med sig
Section titled “Att ta med sig”- Cache API sparar par av
RequestochResponsei namngivna cachar, och finns både i service workern och på sidan. event.respondWith()besvarar en förfrågan. Låt bli att anropa den för det som inte ska cachas, somPOSToch förfrågningar till andra origins.- Network first passar för det som kan ändras under samma adress, som dokumentet. Cache first passar för Vites filer med hash i namnet.
- Spara bara lyckade svar, och
clone()svaret som både sparas och returneras. - Filerna under
/assets/cachas under körning, och applikationen fungerar offline från andra besöket. - Ge cachen ett prefix och en version, och ta bara bort applikationens egna gamla cachar.
Vidare läsning
Section titled “Vidare läsning”- Cache och FetchEvent: respondWith() på MDN.
- Caching på MDN, om fler strategier, som stale-while-revalidate.
- vite-plugin-pwa, om du vill att bygget ska skriva listan över filer åt dig. Det är en orientering, inte något kursen kräver.