Hoppa till innehåll

Service workers

En service worker är det som gör en webbapplikation pålitlig. Den här artikeln går igenom vad den är, hur du lägger till en i ett Vite-projekt och hur den lever sitt liv, från installation till att den ersätts av en nyare version. Vad den gör med förfrågningarna, att cacha filer så att applikationen fungerar offline, är ämnet för nästa artikel. Den bygger på föregående artikel, Progressiva webbapplikationer.

Exemplen bygger på memoryspelet från kursvecka 14, som får fungera offline.

Tidsåtgång: Ungefär 15 minuter.

En service worker är en JavaScript-fil som webbläsaren kör separat från sidan. När den väl styr en sida passerar alla förfrågningar från sidan genom den: dokumentet, skripten, stilmallarna, bilderna och dina egna fetch()-anrop. För varje förfrågan får service workern en fetch-händelse, och kan välja att svara själv, till exempel från en cache, att skicka förfrågan vidare till nätverket eller att göra båda.

Den skiljer sig från koden på sidan på flera sätt:

  • Den har inget fönster. Det finns ingen window och ingen document, och därmed ingen DOM. Det globala objektet heter self.
  • Den har bara asynkrona API:er. fetch(), Cache API och IndexedDB finns, men inte localStorage, som är synkront.
  • Den lever bara när den behövs. Webbläsaren startar den när en händelse ska hanteras och stänger den när den har varit overksam en stund. Ett värde i en variabel kan därför vara borta nästa gång en händelse kommer. Det som ska finnas kvar sparar du i Cache API eller IndexedDB.
  • Den styr flera sidor. En service worker gäller alla flikar med sidor inom dess räckvidd, inte en enskild flik.

Eftersom den kan se och ändra varje förfrågan kräver webbläsaren en säker kontext, alltså HTTPS. Undantaget är localhost, så att du kan utveckla utan certifikat.

En service worker styr sidorna inom sin räckvidd, sitt scope. Räckvidden är som standard mappen där filen ligger, och kan inte vara större än så. En fil på /sw.js styr alla sidor på origin, medan en fil på /js/sw.js bara styr sidor vars adress börjar med /js/.

Därför ska filen ligga i roten, och i ett Vite-projekt betyder det public/:

public/
├── favicon.ico
├── manifest.webmanifest
├── sw.js
└── icons/

Vite kopierar public/sw.js till dist/sw.js som den är, utan att bunta ihop den eller ge den en hash i namnet. Både under npm run dev och efter bygget nås den på /sw.js. En fil i src/ hade däremot bara kommit med i bygget om din kod importerade den, och då under /assets/ med en hash i namnet. Webbläsaren behöver ett namn som är detsamma varje gång, för att kunna se om filen har ändrats.

Eftersom filen inte går genom Vite kan den inte importera moduler från src/, och npm run lint, som bara granskar src/, ser den inte. Skriv den lika noggrant ändå, med JSDoc.

Du vill hålla ordning på dina filer och lägger service workern i public/js/sw.js, och registrerar den med navigator.serviceWorker.register('./js/sw.js'). Registreringen lyckas utan fel. Vilka sidor styr den?

Inga av de sidor du bryr dig om. Filen nås på /js/sw.js, och räckvidden blir /js/. Dokumentet, /, ligger utanför, och förfrågningarna från det passerar aldrig service workern.

Registreringen lyckas eftersom det inte är fel att ha en service worker med liten räckvidd. Felet märks först när applikationen inte fungerar offline. I utvecklarverktygen, under Application → Service workers, syns räckvidden som Scope.

Sidan registrerar service workern. I memoryspelet gör index.js det, efter koden som redan finns där:

js/index.js
import './components/memory-game/index.js'
// ... the code that wires up the page
if ('serviceWorker' in navigator && import.meta.env.PROD) {
try {
await navigator.serviceWorker.register('./sw.js')
} catch (error) {
console.error('The service worker could not be registered:', error)
}
}

'serviceWorker' in navigator är kontrollen från föregående artikel: saknas stödet fungerar spelet ändå, bara inte offline. Adressen ./sw.js räknas från dokumentets adress, och pekar på filen i roten.

import.meta.env.PROD är true i det som npm run build bygger, och false under npm run dev. Vites utvecklingsserver levererar dina källfiler en och en och byter ut dem när du sparar. En service worker som cachar dem skulle ge dig gammal kod i webbläsaren, och du skulle leta fel på fel ställe. Registrera den därför bara i bygget, och testa den med npm run build följt av npm run serve, som visar bygget på http://localhost:4173.

Kom ihåg att lagringen delas upp per origin, som du såg i Lagring i webbläsaren. Det gäller även service workers och deras cachar. Alla dina projekt som du testar på localhost:4173 delar origin. En service worker som ett projekt har registrerat där ligger kvar och styr nästa projekt du kör på samma port, tills du tar bort den under Application → Service workers.

Service workern reagerar på händelser i sin egen fil. Tre händelser bär upp livscykeln:

public/sw.js
self.addEventListener('install', (event) => {
// Runs once per version: cache what the application needs to start.
})
self.addEventListener('activate', (event) => {
// Runs when this version takes over: clean up after the previous one.
})
self.addEventListener('fetch', (event) => {
// Runs for every request from a controlled page.
})

Vägen mellan dem ser ut så här:

Tillståndsdiagram över en service workers livscykel. register() eller en ändrad sw.js leder till installing, där install-händelsen cachar i förväg. Uppfylls waitUntil-löftet går den till installed (waiting), som väntar så länge någon flik använder den gamla versionen. Avvisas löftet går den till redundant. När ingen flik använder den gamla versionen, eller skipWaiting() anropas, går den till activating, där activate-händelsen städar gamla cachar, och sedan till activated, där den tar emot fetch-händelser från sidorna den styr. När en nyare version aktiveras blir den redundant.

install och activate får en händelse med metoden waitUntil(). Den tar ett löfte, och håller service workern vid liv tills löftet är avgjort. Avvisas löftet i install misslyckas installationen och den nya versionen kastas. Ingenting går då sönder, den gamla versionen fortsätter som innan.

self.addEventListener('install', (event) => {
event.waitUntil(precache())
})

Utan waitUntil() kan webbläsaren stänga service workern innan cachningen är klar, eftersom händelsen i sig är avklarad så fort lyssnaren har returnerat.

Första gången användaren besöker sidan finns ingen service worker. Sidan laddas som vanligt från nätverket, och registrerar sedan service workern, som installeras och aktiveras. Men den styr inte sidan som redan är laddad: navigator.serviceWorker.controller är null. Först när sidan laddas nästa gång passerar förfrågningarna genom service workern.

Vill du att den ska ta över direkt kan den anropa self.clients.claim() när den aktiveras. Då går förfrågningar som sidan gör därefter genom den, men skripten och bilderna som redan har laddats hämtas inte på nytt. Vad det betyder för vad som finns i cachen ser du i nästa artikel.

Varje gång användaren öppnar en sida som service workern styr kontrollerar webbläsaren om sw.js har ändrats. Skiljer sig filen med så mycket som en byte installeras den nya versionen bredvid den gamla.

Den nya versionen tar inte över direkt. Den installeras och väntar sedan, i tillståndet waiting, så länge någon flik fortfarande använder den gamla. Det är för att en sida som har laddats med den gamla versionens filer inte plötsligt ska få svar från en ny version som kanske cachar på ett annat sätt.

Du ändrar något i public/sw.js, kör npm run build och laddar om sidan på localhost:4173. Du laddar om en gång till. Service workern beter sig fortfarande som innan. Varför?

Den nya versionen är installerad men väntar. Den gamla styr fliken, och att ladda om räcker inte: den nya sidan laddas medan den gamla fortfarande finns, så fliken släpper aldrig den gamla versionen.

Stäng alla flikar med applikationen och öppna den igen, eller klicka på skipWaiting under Application → Service workers i utvecklarverktygen. Under utveckling är det enklast att kryssa i Update on reload där, så att varje omladdning installerar och aktiverar den senaste versionen.

Service workern kan också hoppa över väntan själv genom att anropa self.skipWaiting() i install. Det är bekvämt, men betyder att en sida som laddades med den gamla versionens filer kan få svar från den nya. Med strategin i nästa artikel, där nätverket alltid frågas först, gör det sällan något.

Fliken Application i Chrome och Edge är där du ser vad service workern gör:

  • Service workers visar vilken version som är aktiv, om någon väntar och vilken räckvidd den har. Här finns Update on reload, Unregister för att ta bort den helt och Offline för att stänga av nätverket för sidan.
  • Cache storage visar cacharna och vad som finns i dem. Mer om dem i nästa artikel.
  • Manifest visar manifestet, från föregående artikel.

I fliken Network står det (ServiceWorker) i kolumnen Size för förfrågningar som service workern har svarat på. En hård omladdning, med Shift nedtryckt, går förbi service workern helt, så den duger inte för att testa den.

  • En service worker är en fil som webbläsaren kör separat från sidan, och som ser varje förfrågan från sidorna den styr. Den har ingen DOM och inget localStorage, och kan stängas när som helst.
  • Räckvidden är mappen filen ligger i. I ett Vite-projekt lägger du den i public/.
  • Registrera den med en kontroll av stödet och bara i bygget, med import.meta.env.PROD. Testa med npm run build och npm run serve.
  • install förbereder, activate städar och fetch hanterar förfrågningarna. waitUntil() håller den vid liv tills arbetet är klart.
  • Första besöket styrs inte av service workern. En ny version väntar tills ingen flik använder den gamla.
CCBY