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 proxy mellan sidan och nätverket
Section titled “En proxy mellan sidan och nätverket”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
windowoch ingendocument, och därmed ingen DOM. Det globala objektet heterself. - Den har bara asynkrona API:er.
fetch(), Cache API och IndexedDB finns, men intelocalStorage, 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.
Var filen ligger
Section titled “Var filen ligger”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.
Fråga: vilka sidor styr den?
Section titled “Fråga: vilka sidor styr den?”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.
Registrera service workern
Section titled “Registrera service workern”Sidan registrerar service workern. I memoryspelet gör index.js det, efter koden som redan finns där:
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.
Livscykeln
Section titled “Livscykeln”Service workern reagerar på händelser i sin egen fil. Tre händelser bär upp livscykeln:
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:
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 besöket
Section titled “Första besöket”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.
När filen ändras
Section titled “När filen ändras”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.
Fråga: varför syns inte ändringen?
Section titled “Fråga: varför syns inte ändringen?”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.
Utvecklarverktygen
Section titled “Utvecklarverktygen”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.
Att ta med sig
Section titled “Att ta med sig”- 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 mednpm run buildochnpm run serve. installförbereder,activatestädar ochfetchhanterar 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.
Vidare läsning
Section titled “Vidare läsning”- Using Service Workers på MDN.
- The service worker lifecycle på web.dev, som
går djupare i uppdateringar och
clients.claim().