Online, offline och Lie-Fi
En pålitlig applikation fungerar utan uppkoppling, och det har du ordnat med service workern i kursvecka 15. Men det svåra är sällan att nätverket saknas helt. Det svåra är när det finns, men inte fungerar. Den här artikeln går igenom hur applikationen kan märka det, hur den slutar vänta och hur den berättar för användaren vad som händer.
Tidsåtgång: Ungefär 12 minuter.
navigator.onLine
Section titled “navigator.onLine”Webbläsaren har en egenskap som ser ut att svara på frågan direkt:
console.log(navigator.onLine) // true
window.addEventListener('offline', () => console.log('offline'))window.addEventListener('online', () => console.log('online'))navigator.onLine är false när webbläsaren tror att enheten saknar nätverk, och händelserna
offline och online utlöses när det ändras. Men webbläsaren vet bara om enheten är ansluten till
ett nätverk, inte om det går att nå din server:
truebetyder inte att något kommer fram. En dator ansluten till ett wifi utan internetförbindelse, eller med ett virtuellt nätverkskort som alltid är anslutet, räknas som online.falsekan också vara fel. På Windows avgörs värdet av om datorn når en server hos Microsoft, och en brandvägg eller VPN kan göra att datorn räknas som offline trots att den når internet.
MDN sammanfattar det med att egenskapen är opålitlig, och att den inte ska användas för att stänga av funktioner, bara för att ge användaren en ledtråd. Den är bra till att visa Du verkar vara offline, men inte till att låta bli att försöka.
Lie-Fi
Section titled “Lie-Fi”Telefonen visar full mottagning, men ingenting laddas. Begreppet Lie-Fi kommer från Jake Archibald, då på Google, och beskriver uppkopplingen som ljuger: den finns, men inget kommer fram, eller så långsamt att det inte spelar någon roll. Det är vanligt på tåg, på en överbelastad mobilmast och på ett hotell-wifi som väntar på att användaren ska logga in.
Lie-Fi är värre än att vara offline. När nätverket saknas misslyckas fetch() direkt, och service
workern svarar från cachen. Med Lie-Fi är navigator.onLine true, fetch() misslyckas inte, och
promiset förblir pending. fetch() har ingen egen timeout, och det kan dröja minuter innan
webbläsaren ger upp. Under tiden ser användaren en tom sida.
Fråga: vad ser användaren?
Section titled “Fråga: vad ser användaren?”Memoryspelet har service workern från kursvecka 15, med network first för dokumentet. Spelaren har spelat flera gånger och allt finns i cachen. Nu sitter spelaren på ett tåg där telefonen har full mottagning, men inga svar kommer fram. Spelaren öppnar spelet. Vad händer?
Ingenting, länge. Service workern frågar nätverket först, och fetch() väntar på ett svar som inte
kommer. Det är först när webbläsaren ger upp och fetch() misslyckas som service workern svarar med
kopian i cachen.
Utan uppkoppling alls hade spelet startat direkt, eftersom fetch() då misslyckas på en gång.
Network first är pålitligt när nätverket saknas, men inte när det ljuger.
En timeout för fetch()
Section titled “En timeout för fetch()”Lösningen är att inte vänta hur länge som helst. AbortSignal.timeout() ger en signal som avbryter
efter ett visst antal millisekunder, och den ges till fetch() på samma sätt som signalen från en
AbortController i Nätverksanrop med fetch:
try { const response = await fetch(url, { signal: AbortSignal.timeout(8000) }) // ...} catch (error) { if (error.name === 'TimeoutError') { status.textContent = 'Servern svarar inte. Försök igen om en stund.' }}När tiden har gått blir promiset rejected med ett DOMException som heter TimeoutError, inte
AbortError. Skillnaden är användbar. AbortError betyder att din egen kod har avbrutit, och det ska
ignoreras, som i kursvecka 12. TimeoutError betyder att servern inte svarade, och det ska användaren
få veta.
Har komponenten redan en AbortController för förfrågningar som kan bli inaktuella, kombinerar du signalerna
med AbortSignal.any(). Förfrågan avbryts av den som kommer först:
const signal = AbortSignal.any([this.#abortController.signal, AbortSignal.timeout(8000)])const response = await fetch(url, { signal })Hur lång timeouten ska vara beror på vad användaren väntar på. En sökning som tar mer än några sekunder känns trasig. En uppladdning av en stor fil kan ta minuter, och behöver ingen timeout alls, eller en mycket generös.
Network first med timeout
Section titled “Network first med timeout”Samma signal fungerar i service workern. Det är den enda ändringen som behövs i networkFirst() från
kursvecka 15:
const NETWORK_TIMEOUT_MS = 3000
async function networkFirst(request) { const cache = await caches.open(CACHE_NAME)
try { const response = await fetch(request, { signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS) }) 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 }}Svarar nätverket inte inom tre sekunder avbryts fetch(), och felet hamnar i catch, precis som
när nätverket saknas. Har du lagt till navigate() från Routing i en SPA
gäller timeouten också för navigeringarna, eftersom navigate() anropar networkFirst():
Med Lie-Fi startar spelet nu efter tre sekunder, med kopian i cachen.
Fråga: varför inte en halv sekund?
Section titled “Fråga: varför inte en halv sekund?”Tre sekunder är länge att vänta. Varför inte sätta NETWORK_TIMEOUT_MS till 500?
På en långsam men fungerande uppkoppling hinner svaret sällan komma inom en halv sekund. Service workern svarar då nästan alltid från cachen, och användaren får den gamla versionen även när den nya gick att hämta. Network first blir i praktiken cache first, med problemet du såg i Offline med Cache API.
Timeouten är en avvägning mellan att vänta på nätverket och att visa något direkt. Ett par sekunder
är vanligt. Workbox, som du mötte i kursvecka 15, kallar inställningen networkTimeoutSeconds.
Eftersom sw.js har ändrats installeras den nya versionen av sig själv, och väntar som vanligt tills
ingen flik använder den gamla. Timeouten ändrar inte vad som finns i cachen, så CACHE_NAME
behöver inte byta version för den här ändringens skull.
När anslutningen bryts
Section titled “När anslutningen bryts”En WebSocket, som i Messages i B3, är inget fetch()-anrop. Den passerar inte service workern, och
den kan inte svaras på från en cache. Det som går att göra är att märka när anslutningen bryts, och
försöka igen.
När anslutningen stängs, av vilket skäl det än är, utlöses close på socketen. Föregås det av ett
fel utlöses error först, men close kommer alltid efteråt, så det räcker att lyssna på den. Att
försöka igen direkt, om och om igen, belastar servern och telefonens batteri. Vänta i stället allt
längre mellan försöken, och börja om när det lyckas:
const MIN_DELAY_MS = 1000const MAX_DELAY_MS = 30000
let delay = MIN_DELAY_MS
/** * Opens a connection to the server, and opens a new one whenever it closes. */function connect() { const socket = new WebSocket('wss://example.com/chat')
socket.addEventListener('open', () => { delay = MIN_DELAY_MS status.textContent = '' })
socket.addEventListener('close', () => { status.textContent = 'Anslutningen bröts. Försöker igen …' setTimeout(connect, delay) delay = Math.min(delay * 2, MAX_DELAY_MS) })}Väntetiden fördubblas för varje misslyckat försök, en sekund, två, fyra och så vidare upp till 30
sekunder. Det kallas exponential backoff. I en komponent lägger du anslutningen i
connectedCallback() och stänger den i disconnectedCallback(), och då ska den inte öppnas igen,
så komponenten behöver hålla reda på om den fortfarande är ansluten till DOM:en.
Händelsen online kan användas som en ledtråd att försöka direkt, i stället för att vänta ut
väntetiden. Men sluta inte försöka bara för att navigator.onLine är false.
Återanslutning är inget krav på Messages i B3, men ett bra exempel på en egen funktion. Servern i B3
skickar dessutom ett heartbeat var 40:e sekund. Har inget kommit på betydligt längre tid än så är
anslutningen troligen död, även om close aldrig har utlösts, och det är Lie-Fi för en WebSocket.
Berätta för användaren
Section titled “Berätta för användaren”Vad applikationen än gör när nätverket sviktar, ska användaren förstå vad som händer. Några riktlinjer:
- Visa läget, dölj det inte. Anslutningen bröts, försöker igen är bättre än en knapp som inte gör något. Meddelandet som inte gick att skicka ska synas som osänt, inte försvinna.
- Bygg på det som faktiskt händer. Ett
fetch()som misslyckas eller en WebSocket som stängs är säkra besked.navigator.onLineär en gissning. - Låt användaren fortsätta där det går. Memory behöver inget nätverk. Det ska inte gå att spela sämre för att chatten i fönstret bredvid är frånkopplad.
- Gör texten tillgänglig. Ett element med
role="status"läses upp av skärmläsare när texten ändras, utan att flytta fokus.
<p id="status" role="status"></p>Att ta med sig
Section titled “Att ta med sig”navigator.onLinevet bara om enheten är ansluten till ett nätverk. Använd den som en ledtråd, inte för att stänga av funktioner.- Lie-Fi är en uppkoppling som finns men inte fungerar.
fetch()väntar då utan timeout. AbortSignal.timeout()sätter en timeout och ger ettTimeoutError. Kombinera med din egen signal medAbortSignal.any().- Ge network first i service workern en timeout på ett par sekunder, så att applikationen startar från cachen när nätverket ljuger.
- En WebSocket som stängs öppnas igen med allt längre väntetid mellan försöken.
- Visa vad som händer, och bygg det på det som faktiskt har hänt.
Vidare läsning
Section titled “Vidare läsning”- Navigator: onLine på MDN, om varför egenskapen är opålitlig.
- AbortSignal.timeout() och AbortSignal.any() på MDN.
- WebSocket: close event på MDN.