Making A Service Worker: A Case Study

A szerzőről

Lyza Danger Gardner egy dev. Mióta 2007-ben társalapítója volt a portlandi, oregoni székhelyű Cloud Four nevű mobil webes startupnak, kínozza és izgatja magát a …További információLyza↬

  • 23 perc olvasás
  • Kódolás,JavaScript,technikák,szervizmunkások
  • Off-line olvasásra mentve
  • Megosztás Twitteren, LinkedIn
Ez a cikk elmagyarázza, mi az a service worker, és hogyan állíthatjuk össze a sajátunkat, regisztrálva, telepítve és aktiválva minden gond nélkül.

Nem hiányzik a felhajtás és az izgalom az újonnan születő service worker API-val kapcsolatban, amelyet már néhány népszerű böngésző is szállít. Vannak szakácskönyvek és blogbejegyzések, kódrészletek és eszközök. De én úgy találom, hogy amikor alaposan meg akarok tanulni egy új webes koncepciót, gyakran ideális, ha felhajtom a közmondásos ingujjamat, belevetem magam és a semmiből építek valamit.

A zökkenők, zúzódások, buktatók és hibák, amelyekbe ezúttal belefutottam, előnyökkel járnak: Most már sokkal jobban értem a szervizmunkásokat, és egy kis szerencsével segíthetek elkerülni néhány fejfájást, amivel az új API-val való munka során találkoztam.

A szervizmunkások sokféle dolgot csinálnak; számtalan módja van annak, hogy kihasználjuk az erejüket. Úgy döntöttem, hogy építek egy egyszerű service worker-t a (statikus, nem bonyolult) webhelyemhez, amely nagyjából tükrözi azokat a funkciókat, amelyeket az elavult Application Cache API korábban nyújtott – vagyis:

  • a webhely offline működésre késztetése,
  • az online teljesítmény növelése bizonyos eszközök hálózati kéréseinek csökkentésével,
  • egy testreszabott offline visszaesési élmény biztosítása.

Mielőtt elkezdeném, szeretnék köszönetet mondani két embernek, akiknek a munkája ezt lehetővé tette. Először is, óriási hálával tartozom Jeremy Keith-nek a szolgáltatásmunkások implementálásáért a saját weboldalán, ami a saját kódom kiindulópontjaként szolgált. A közelmúltban írt bejegyzése inspirált, amelyben leírta folyamatban lévő szervizmunkás tapasztalatait. Valójában az én munkám annyira erősen származékos, hogy nem is írtam volna róla, ha Jeremy egy korábbi bejegyzésében nem figyelmeztetett volna:

Szóval, ha úgy döntesz, hogy a Service Workerekkel játszol, kérlek, kérlek oszd meg a tapasztalataidat.

Másrészt, mindenféle nagy köszönet Jake Archibaldnak a kiváló technikai felülvizsgálatért és visszajelzésért. Mindig jó, ha a Service Worker specifikáció egyik alkotója és evangelistája képes egyenesbe hozni!

Mi a Service Worker?

A Service Worker egy olyan szkript, amely a weboldalad és a hálózat között áll, és többek között lehetőséget ad arra, hogy elfogd a hálózati kéréseket, és különböző módon válaszolj rájuk.

A weboldalad vagy alkalmazásod működéséhez a böngésző lehívja az eszközöket – például HTML oldalakat, JavaScriptet, képeket, betűtípusokat. A múltban ennek kezelése elsősorban a böngésző előjoga volt. Ha a böngésző nem tudott hozzáférni a hálózathoz, akkor valószínűleg a “Hé, offline vagy” üzenetét látta. Voltak technikák, amelyekkel ösztönözni lehetett az eszközök helyi gyorsítótárazását, de gyakran a böngészőé volt az utolsó szó.

Ez nem volt túl jó élmény az offline felhasználók számára, és a webfejlesztők számára kevés kontrollt hagyott a böngésző gyorsítótárazása felett.

Az Application Cache (vagy AppCache), amelynek megjelenése néhány évvel ezelőtt ígéretesnek tűnt. Látszólag lehetővé tette, hogy megszabja, hogyan kell kezelni a különböző eszközöket, hogy a webhely vagy az alkalmazás offline is működhessen. Az AppCache egyszerűnek tűnő szintaxisa azonban meghazudtolta a mögötte rejlő zavaros természetet és a rugalmasság hiányát.

A szervizmunkás API képes arra, amire az AppCache, és még sokkal többre is. De elsőre kicsit ijesztőnek tűnik. A specifikációk nehézkes és absztrakt olvasmányt jelentenek, és számos API alárendelődik neki, vagy más módon kapcsolódik hozzá: cache, fetch stb. A szolgáltatásmunkások nagyon sok funkciót foglalnak magukban: push értesítések és hamarosan a háttérben történő szinkronizálás. Az AppCache-hez képest ez… bonyolultnak tűnik.

Míg az AppCache (ami egyébként megszűnik) könnyen megtanulható volt, de utána minden egyes pillanatban szörnyű volt (véleményem szerint), a service workerek inkább kezdeti kognitív befektetést jelentenek, de erősek és hasznosak, és általában ki tudod húzni magad a bajból, ha elrontasz dolgokat.

Minden alapvető Service Worker fogalom

A service worker egy fájl, amiben van némi JavaScript. Ebbe a fájlba úgy írhatsz JavaScriptet, ahogy ismered és szereted, néhány fontos dolgot azonban szem előtt kell tartanod.

A szervizmunkás szkriptek a böngészőben az általuk vezérelt oldalaktól külön szálban futnak. Vannak módok a munkások és az oldalak közötti kommunikációra, de ezek külön hatókörben hajtódnak végre. Ez azt jelenti, hogy nem fér hozzá például az oldalak DOM-jához. Én úgy képzelem el a szervizmunkást, mintha egyfajta külön lapon futna az általa érintett oldaltól; ez egyáltalán nem pontos, de hasznos durva metafora ahhoz, hogy elkerüljem a zavart.

A szervizmunkásban lévő JavaScript nem blokkolhat. Aszinkron API-kat kell használnia. Például nem használhatod a localStorage-t egy service workerben (a localStorage egy szinkron API). Humoros módon, még ha ezt tudtam is, sikerült ezt megszegnem, ahogy majd látni fogjuk.

Szolgálati munkás regisztrálása

Egy szolgálati munkást a regisztrálásával léptetsz életbe. Ez a regisztráció a szervizmunkáson kívülről történik, a weboldalad egy másik oldala vagy szkriptje által. Az én weboldalamon egy globális site.js szkript szerepel minden HTML-oldalon. Onnan regisztrálom a szervizmunkásomat.

A szervizmunkás regisztrálásakor (opcionálisan) azt is megmondod neki, hogy milyen hatókörre alkalmazza magát. Megbízhatod a szervizmunkást, hogy csak a weboldalad egy részének dolgait kezelje (például '/blog/'), vagy regisztrálhatod az egész weboldaladra ('/'), ahogy én is teszem.

Szervizmunkás életciklusa és eseményei

A szervizmunkás munkájának nagy részét úgy végzi, hogy figyel a releváns eseményekre, és hasznos módon reagál rájuk. A szervizmunkás életciklusának különböző pontjain különböző események válthatók ki.

A szervizmunkás regisztrálása és letöltése után a háttérben települ. A szervizmunkás figyelhet a install eseményre, és elvégezheti az adott szakasznak megfelelő feladatokat.

A mi esetünkben a install állapotot arra szeretnénk kihasználni, hogy előzetesen gyorsítótárba helyezzünk egy csomó olyan eszközt, amelyekről tudjuk, hogy később offline is elérhetőek lesznek.

Az install szakasz befejezése után a szervizmunkás aktiválódik. Ez azt jelenti, hogy a szervizmunkás most már a scope-jén belül irányítja a dolgokat, és teheti a dolgát. Az activate esemény nem túl izgalmas egy új szervizmunkás esetében, de látni fogjuk, hogy mennyire hasznos, amikor egy szervizmunkást frissítünk egy új verzióval.

Az, hogy pontosan mikor történik az aktiválás, attól függ, hogy egy teljesen új szervizmunkásról vagy egy már meglévő szervizmunkás frissített verziójáról van-e szó. Ha a böngészőnek nincs már regisztrálva egy adott szervizmunkás korábbi verziója, akkor az aktiválás a telepítés befejezése után azonnal megtörténik.

A telepítés és az aktiválás befejezése után nem történik meg újra, amíg a szervizmunkás egy frissített verzióját le nem töltjük és regisztráljuk.

A telepítésen és az aktiváláson túl ma elsősorban a fetch eseményt fogjuk megvizsgálni, hogy a szervizmunkásunk hasznos legyen. De ezen túl is számos hasznos esemény létezik: például a szinkronizálási események és az értesítési események.

Extraként vagy szabadidős szórakozásként többet olvashatsz a szervizmunkások által megvalósított interfészekről. A szervizmunkások ezen interfészek implementálásával kapják az eseményeik nagy részét és a kibővített funkcionalitásuk nagy részét.

A szervizmunkás ígéretalapú API-ja

A szervizmunkás API nagymértékben használja a Promises. Az ígéret egy aszinkron művelet esetleges eredményét reprezentálja, még akkor is, ha a tényleges értéket csak akkor tudjuk meg, ha a művelet valamikor a jövőben fejeződik be.

getAnAnswerToADifficultQuestionSomewhereFarAway() .then(answer => { console.log('I got the ${answer}!'); }) .catch(reason => { console.log('I tried to figure it out but couldn't because ${reason}');});

A getAnAnswer… függvény egy Promise-et ad vissza, amely (reméljük) végül teljesül, vagy feloldódik a keresett answer-re. Ezután ez a answer betáplálható bármelyik láncolt then kezelőfüggvényhez, vagy sajnálatos esetben, ha nem éri el a célját, a Promise visszautasítható – gyakran indoklással – és a catch kezelőfüggvények gondoskodhatnak az ilyen helyzetekről.

Az ígéretekhez több is tartozik, de itt megpróbálom a példákat egyszerűnek (vagy legalábbis kommentáltnak) tartani. Javaslom, hogy ha még nem ismeri az ígéreteket, olvasson el néhány informatív olvasmányt.

Figyelem: A szolgáltatásmunkásokhoz használt mintakódban azért használok bizonyos ECMAScript6 (vagy ES2015) funkciókat, mert a szolgáltatásmunkásokat támogató böngészők is támogatják ezeket a funkciókat. Konkrétan itt nyílfüggvényeket és sablonsztringeket használok.

Egyéb szolgáltatási munkások szükségességei

Megjegyezzük továbbá, hogy a szolgáltatási munkások működéséhez HTTPS szükséges. Van egy fontos és hasznos kivétel ez alól a szabály alól: Ez megkönnyebbülés, mert a helyi SSL beállítása néha nehézkes.

Mókás tény: Ez a projekt arra kényszerített, hogy megtegyek valamit, amit már egy ideje halogattam: SSL-t szerezzek és konfiguráljak a webhelyem www aldomainjéhez. Ez olyasvalami, amit sürgetem az embereket, hogy fontolják meg, mert a jövőben a böngészőbe kerülő új dolgok nagyjából mindegyike megköveteli az SSL használatát.

A Chrome-ban (én a 47-es verziót használom) ma már minden működik, amit összerakunk. Bármelyik nap megjelenhet a Firefox 44, ami támogatja a szervizmunkásokat. Az Is Service Worker Ready? részletes információt nyújt a különböző böngészők támogatásáról.

Regisztrálás, telepítés és aktiválás a szolgáltatásmunkásnál

Most, hogy elintéztünk néhány elméleti kérdést, elkezdhetjük összerakni a szolgáltatásmunkásunkat.

Szolgálati munkásunk telepítéséhez és aktiválásához a install és activate eseményeket akarjuk figyelni, és ezekre reagálni.

Elkezdhetünk egy üres fájlal a szolgálati munkásunk számára, és hozzáadhatunk néhány eventListeners-et. A serviceWorker.js-ben:

self.addEventListener('install', event => { // Do install stuff});self.addEventListener('activate', event => { // Do activate stuff: This will come later on.});

Szolgálati munkásunk regisztrálása

Most meg kell mondanunk a weboldalunk oldalainak, hogy használják a szervizmunkást.

Memlékezzünk, ez a regisztráció a szervizmunkáson kívülről történik – az én esetemben egy szkriptből (/js/site.js), amely a weboldalam minden oldalán szerepel.

Az én site.js-emben:

if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/serviceWorker.js', { scope: '/' });}

Statikus eszközök előzetes gyorsítótárazása a telepítés során

A telepítési szakaszban szeretnék néhány eszközt előre gyorsítótárazni a weboldalamon.

  • A weboldalam számos oldala által használt statikus eszközök (képek, CSS, JavaScript) előzetes gyorsítótárazásával felgyorsíthatom a betöltési időt, ha ezeket a gyorsítótárból hívom elő, ahelyett, hogy a hálózatról hívnám le a következő oldalletöltésekkor.
  • Az offline tartalékoldal előzetes gyorsítótárazásával egy szép oldalt tudok megjeleníteni, ha nem tudok teljesíteni egy oldalkérést, mert a felhasználó offline van.

Az ehhez szükséges lépések a következők:

  1. Mondjuk meg a install eseménynek, hogy várjon, és ne fejezze be, amíg el nem végeztem, amit kell, a event.waitUntil segítségével.
  2. Nyissuk meg a megfelelő cache-et, és a Cache.addAll segítségével ragasszuk bele a statikus eszközöket. A progresszív webalkalmazás nyelvén ezek az eszközök alkotják az “alkalmazáshéjamat”.

A /serviceWorker.js-ben bővítsük ki a install kezelőt:

self.addEventListener('install', event => { function onInstall () { return caches.open('static') .then(cache => cache.addAll() ); } event.waitUntil(onInstall(event));});

A szervizmunkás megvalósítja a CacheStorage interfészt, ami a caches tulajdonságot globálisan elérhetővé teszi a szervizmunkásunkban. A caches-nek számos hasznos metódusa van – például a open és a delete.

A Promises-t itt láthatjuk munka közben: caches.open egy Promise feloldást ad vissza egy cache objektumra, amint sikeresen megnyitotta a static gyorsítótárat; addAll szintén egy Promise feloldást ad vissza, amely akkor oldódik fel, ha az összes neki átadott elemet elraktározta a gyorsítótárban.

Megmondom a event-nak, hogy várjon, amíg a kezelőfüggvényem által visszaadott Promise feloldása sikeres lesz. Ekkor biztosak lehetünk benne, hogy az összes előzetesen gyorsítótárba helyezett elem rendeződik, mielőtt a telepítés befejeződik.

Konzol zavarok

Stale naplózás

Vélhetően nem hiba, de biztosan zavaró: Ha console.log a szervizmunkásokról, a Chrome a következő oldalkéréseknél továbbra is újra megjeleníti (ahelyett, hogy törölné) ezeket a naplóüzeneteket. Ez azt a látszatot keltheti, mintha az események túl sokszor lépnének működésbe, vagy mintha a kód újra és újra végrehajtódna.

Adjunk például egy log utasítást a install kezelőnkhöz:

self.addEventListener('install', event => { // … as before console.log('installing');});
A Chrome 47-től kezdve a “telepítés” naplóüzenet továbbra is megjelenik a következő oldalkéréseknél. A Chrome valójában nem lövi ki a install eseményt minden oldalbetöltéskor. Ehelyett elavult naplóbejegyzéseket jelenít meg. (Nagy verzió megtekintése)

Egy hiba, amikor a dolgok rendben vannak

Egy másik furcsa dolog, hogy miután egy szolgáltatásmunkás telepítve és aktiválva van, a hatókörébe tartozó bármely oldal későbbi oldalbetöltései mindig egyetlen hibát okoznak a konzolon. Azt hittem, valamit rosszul csinálok.

A Chrome 47-től kezdve egy már regisztrált szervizmunkással rendelkező oldal elérése mindig ezt a hibát okozza a konzolon. (Nagy verzió megtekintése)

Mit értünk el eddig

A szervizmunkás kezeli a install eseményt és előre tárol néhány statikus eszközt. Ha ezt a szervizmunkást használnánk és regisztrálnánk, akkor valóban előre gyorsítótárazná a jelzett eszközöket, de még nem tudná offline kihasználni őket.

A serviceWorker.js tartalma a GitHubon található.

Fetch Handling With Service Workers

Szervizmunkásunknak eddig van egy kidolgozott install kezelője, de ezen túl nem csinál semmit. A szervizmunkásunk varázsa valójában akkor fog megtörténni, amikor fetch eseményeket váltunk ki.

A lehívásokra többféleképpen is reagálhatunk. Különböző hálózati stratégiák használatával megmondhatjuk a böngészőnek, hogy mindig próbáljon meg bizonyos eszközöket a hálózatról lekérni (biztosítva, hogy a kulcsfontosságú tartalmak frissek legyenek), míg a statikus eszközök esetében a gyorsítótárazott másolatokat részesítjük előnyben – ezzel valóban karcsúsíthatjuk az oldalunk hasznos terhelését. Egy szép offline tartalékot is biztosíthatunk, ha minden más nem sikerül.

Amikor egy böngésző le akar hívni egy olyan eszközt, amely ennek a szervizmunkásnak a hatókörébe tartozik, hallani fogunk róla, igen, egy eventListener hozzáadásával a serviceWorker.js-ben:

self.addEventListener('fetch', event => { // … Perhaps respond to this fetch in a useful way?});

Minden olyan lehívás, amely ennek a szervizmunkásnak a hatókörébe (azaz az útvonalba) esik, kiváltja ezt az eseményt – HTML oldalak, szkriptek, képek, CSS, amit csak akarsz. Szelektíven kezelhetjük, hogy a böngésző hogyan reagál bármelyik ilyen lehívásra.

Should We Handle This Fetch?

Amikor egy eszközhöz fetch esemény lép fel, az első dolog, amit meg akarok határozni, hogy ez a szolgáltatásmunkás beavatkozzon-e az adott erőforrás lehívásába. Ellenkező esetben ne tegyen semmit, és hagyja, hogy a böngésző érvényesítse az alapértelmezett viselkedését.

Az serviceWorker.js-ben ilyen alapvető logikával fogunk végezni:

self.addEventListener('fetch', event => { function shouldHandleFetch (event, opts) { // Should we handle this fetch? } function onFetch (event, opts) { // … TBD: Respond to the fetch } if (shouldHandleFetch(event, config)) { onFetch(event, config); }});

A shouldHandleFetch függvény értékeli az adott kérést, hogy meghatározza, adjunk-e választ, vagy hagyjuk, hogy a böngésző érvényesítse az alapértelmezett kezelését.

Miért nem használunk ígéreteket?

A szolgáltatásmunkás ígéretek iránti előszeretetét követve, az fetch eseménykezelőm első verziója így nézett ki:

self.addEventListener('fetch', event => { function shouldHandleFetch (event, opts) { } function onFetch (event, opts) { } shouldHandleFetch(event, config) .then(onFetch(event, config)) .catch(…);});

Logikusnak tűnik, de elkövettem néhány kezdő hibát az ígéretekkel. Esküszöm, hogy már kezdetben is éreztem egy kódszagot, de Jake volt az, aki ráébresztett a hibáimra. (Tanulság: Mint mindig, ha a kódot rossznak érzed, akkor valószínűleg az is.)

Az ígéretek visszautasítását nem szabad arra használni, hogy jelezzük: “olyan választ kaptam, ami nem tetszett”. Ehelyett az elutasításoknak azt kell jelezniük, hogy “Á, a francba, valami rosszul sült el a válasz megszerzése közben”. Vagyis az elutasításnak kivételesnek kell lennie.

Az érvényes kérések kritériumai

Rendben, vissza annak meghatározásához, hogy egy adott lekérdezési kérés alkalmazható-e a szolgáltatási munkásom számára. A webhelyspecifikus kritériumaim a következők:

  1. A kért URL-nek olyasvalamit kell képviselnie, amit gyorsítótárba akarok helyezni vagy amire válaszolni akarok. Az elérési útvonalának meg kell egyeznie az Regular Expression érvényes elérési útvonalakkal.
  2. A kérés HTTP-módszerének GET-nek kell lennie.
  3. A kérésnek az én eredetemből (lyza.com) származó erőforrásra kell irányulnia.

Ha a criteria tesztek bármelyike false-ra értékelődik, akkor ezt a kérést nem szabad kezelni. In serviceWorker.js:

function shouldHandleFetch (event, opts) { var request = event.request; var url = new URL(request.url); var criteria = { matchesPathPattern: !!(opts.cachePathPattern.exec(url.pathname), isGETRequest : request.method === 'GET', isFromMyOrigin : url.origin === self.location.origin }; // Create a new array with just the keys from criteria that have // failing (i.e. false) values. var failingCriteria = Object.keys(criteria) .filter(criteriaKey => !criteria); // If that failing array has any length, one or more tests failed. return !failingCriteria.length;}

Az itt szereplő kritériumok természetesen az én sajátjaim, és webhelyenként változnának. A event.request egy Request objektum, amely mindenféle adatot tartalmaz, amit megnézhetsz, hogy felmérd, hogyan szeretnéd, hogy a lekérdezés kezelője viselkedjen.

Triviális megjegyzés: Ha észrevetted a config behatolását, amelyet opts-ként adtunk át a kezelő függvényeknek, jól észrevetted. Kiszámoltam néhány újrafelhasználható config-szerű értéket, és létrehoztam egy config objektumot a szolgáltatásmunkás legfelső szintű hatókörében:

var config = { staticCacheItems: , cachePathPattern: /^\/(?:(20{2}|about|blog|css|images|js)\/(.+)?)?$/};

Miért fehér lista?

Elgondolkodhatsz azon, hogy miért csak olyan dolgokat gyorsítótárazok, amelyek elérési útvonala megfelel ennek a szabályos kifejezésnek:

/^\/(?:(20{2}|about|blog|css|images|js)\/(.+)?)?$/

… ahelyett, hogy mindent gyorsítótáraznék, ami a saját eredetemből jön. Néhány ok:

  • Nem akarom magát a szervizmunkást gyorsítótárba helyezni.
  • Mikor lokálisan fejlesztem a webhelyemet, néhány generált kérés olyan dolgokra vonatkozik, amelyeket nem akarok gyorsítótárba helyezni. Például a browserSync-t használom, ami egy csomó kapcsolódó kérést indít el a fejlesztői környezetemben. Nem akarom ezeket a dolgokat gyorsítótárba helyezni! Zűrösnek és kihívásnak tűnt, hogy megpróbáltam végiggondolni mindent, amit nem akarok gyorsítótárba helyezni (nem is beszélve arról, hogy egy kicsit furcsa, hogy ezt ki kell írnom a szervizmunkásom konfigurációjában). Így a fehérlistás megközelítés sokkal természetesebbnek tűnt.

Writing The Fetch Handler

Most készen állunk arra, hogy az alkalmazható fetch kéréseket átadjuk egy kezelőnek. A onFetch függvénynek meg kell határoznia:

  1. milyen erőforrást kérnek,
  2. és hogyan kell teljesítenem ezt a kérést.

1. Milyen erőforrást kérnek?

Az HTTP Accept fejlécből megtudhatom, hogy milyen típusú eszközt kérnek. Ez segít kitalálni, hogyan akarom kezelni.

function onFetch (event, opts) { var request = event.request; var acceptHeader = request.headers.get('Accept'); var resourceType = 'static'; var cacheKey; if (acceptHeader.indexOf('text/html') !== -1) { resourceType = 'content'; } else if (acceptHeader.indexOf('image') !== -1) { resourceType = 'image'; } // {String} cacheKey = resourceType; // … now do something}

A rendezettség érdekében különböző típusú erőforrásokat akarok különböző gyorsítótárakba tenni. Ez lehetővé teszi számomra, hogy később kezeljem ezeket a gyorsítótárakat. Ezek a cache-kulcsok String tetszőlegesek – a cache-eket úgy hívhatod, ahogy akarod; a cache API-nak nincs véleménye.

2. Válaszolj a Fetch

A következő dolog, amit onFetch tenned kell, hogy respondTo a fetch eseményre egy intelligens Response-mal válaszolj.

function onFetch (event, opts) { // 1. Determine what kind of asset this is… (above). if (resourceType === 'content') { // Use a network-first strategy. event.respondWith( fetch(request) .then(response => addToCache(cacheKey, request, response)) .catch(() => fetchFromCache(event)) .catch(() => offlineResponse(opts)) ); } else { // Use a cache-first strategy. event.respondWith( fetchFromCache(event) .catch(() => fetch(request)) .then(response => addToCache(cacheKey, request, response)) .catch(() => offlineResponse(resourceType, opts)) ); }}

Vigyázz az aszinkronnal!

A mi esetünkben a shouldHandleFetch nem csinál semmit aszinkron, és a onFetch sem tesz semmit a event.respondWith pontig. Ha előtte történt volna valami aszinkron, akkor bajban lennénk. A event.respondWith-et a fetch esemény bekövetkezése és a vezérlésnek a böngészőhöz való visszatérése között kell meghívni. Ugyanez vonatkozik a event.waitUntil-re is. Alapvetően, ha egy eseményt kezelsz, vagy csinálj valamit azonnal (szinkron), vagy mondd meg a böngészőnek, hogy várjon, amíg az aszinkron dolgaid befejeződnek.

HTML Content: Hálózat-első stratégia megvalósítása

A fetch kérésekre való válaszadás magában foglalja a megfelelő hálózati stratégia megvalósítását. Nézzük meg közelebbről, hogyan válaszolunk a HTML-tartalomra (resourceType === 'content') irányuló kérésekre.

if (resourceType === 'content') { // Respond with a network-first strategy. event.respondWith( fetch(request) .then(response => addToCache(cacheKey, request, response)) .catch(() => fetchFromCache(event)) .catch(() => offlineResponse(opts)) );}

A tartalomra irányuló kérések teljesítésének módja itt egy hálózat-első stratégia. Mivel a HTML-tartalom a webhelyem központi kérdése, és gyakran változik, mindig friss HTML-dokumentumokat próbálok beszerezni a hálózatról.

Lépjünk végig ezen.

1. Próbáljuk meg a hálózatról való lehívást

fetch(request) .then(response => addToCache(cacheKey, request, response))

Ha a hálózati kérés sikeres (azaz az ígéret feloldódik), akkor menjünk tovább, és tegyük el a HTML-dokumentum egy példányát a megfelelő gyorsítótárba (content). Ezt nevezzük átolvasásos gyorsítótárazásnak:

function addToCache (cacheKey, request, response) { if (response.ok) { var copy = response.clone(); caches.open(cacheKey).then( cache => { cache.put(request, copy); }); return response; }}

A válaszokat csak egyszer lehet felhasználni.

A birtokunkban lévő response objektummal két dolgot kell tennünk:

  • tárolni,
  • válaszolni vele az eseményre (azaz visszaadni).

De a Response objektumokat csak egyszer lehet használni. Klónozással egy másolatot tudunk létrehozni a gyorsítótár használatára:

var copy = response.clone();

Ne cache-eljük a rossz válaszokat. Ne kövesse el ugyanazt a hibát, mint én. A kódom első verziójában nem volt ez a feltétel:

if (response.ok)

Meglehetősen félelmetes, hogy 404-es vagy más rossz válaszok kerülnek a gyorsítótárba! Csak boldog válaszok a gyorsítótárban.

2. Try to Retrieve From Cache

Ha az eszköz lekérdezése a hálózatról sikerül, akkor kész vagyunk. Ha azonban nem sikerül, akkor lehet, hogy offline vagy más módon veszélyeztetett a hálózat. Próbáljuk meg lekérdezni a HTML egy korábban gyorsítótárazott példányát a gyorsítótárból:

fetch(request) .then(response => addToCache(cacheKey, request, response)) .catch(() => fetchFromCache(event))

Itt a fetchFromCache függvény:

function fetchFromCache (event) { return caches.match(event.request).then(response => { if (!response) { // A synchronous error that will kick off the catch handler throw Error('${event.request.url} not found in cache'); } return response; });}

Megjegyzés: Ne jelezzük, hogy melyik gyorsítótárat szeretnénk ellenőrizni a caches.match segítségével; egyszerre ellenőrizzük az összeset.

3. Provide an Offline Fallback

Ha idáig eljutottunk, de nincs semmi a cache-ben, amivel válaszolhatnánk, adjunk vissza egy megfelelő offline fallbacket, ha lehetséges. HTML oldalak esetében ez az oldal a /offline/ gyorsítótárból származó oldal. Ez egy viszonylag jól formázott oldal, amely közli a felhasználóval, hogy offline van, és hogy nem tudjuk teljesíteni, amit keres.

fetch(request) .then(response => addToCache(cacheKey, request, response)) .catch(() => fetchFromCache(event)) .catch(() => offlineResponse(opts))

És itt van a offlineResponse funkció:

function offlineResponse (resourceType, opts) { if (resourceType === 'image') { return new Response(opts.offlineImage, { headers: { 'Content-Type': 'image/svg+xml' } } ); } else if (resourceType === 'content') { return caches.match(opts.offlinePage); } return undefined;}
Egy offline oldal (nagy változat megtekintése)

Más erőforrások: A cache-first stratégia megvalósítása

A HTML tartalomtól eltérő erőforrások lekérdezési logikája a cache-first stratégiát használja. A képek és más statikus tartalmak a webhelyen ritkán változnak; ezért először a gyorsítótárat ellenőrizze, és kerülje el a hálózati körutazást.

event.respondWith( fetchFromCache(event) .catch(() => fetch(request)) .then(response => addToCache(cacheKey, request, response)) .catch(() => offlineResponse(resourceType, opts)));

A lépések itt a következők:

  1. próbálja lekérni az eszközt a gyorsítótárból;
  2. ha ez nem sikerül, próbálja meg a hálózatról lekérni (átolvasásos gyorsítótárral);
  3. ha ez nem sikerül, adjon meg egy offline tartalék erőforrást, ha lehetséges.

Offline kép

A offlineResource függvény befejezésével visszaadhatunk egy SVG képet “Offline” szöveggel offline fallbackként:

function offlineResponse (resourceType, opts) { if (resourceType === 'image') { // … return an offline image } else if (resourceType === 'content') { return caches.match('/offline/'); } return undefined;}

És végezzük el a config megfelelő frissítéseit:

var config = { // … offlineImage: '<svg role="img" aria-labelledby="offline-title"' + 'viewBox="0 0 400 300" xmlns="http://www.w3.org/2000/svg">' + '<title>Offline</title>' + '<g fill="none" fill-rule="evenodd"><path fill=>"#D8D8D8" d="M0 0h400v300H0z"/>' + '<text fill="#9B9B9B" font-family="Times New Roman,Times,serif" font-size="72" font-weight="bold">' + '<tspan x="93" y="172">offline</tspan></text></g></svg>', offlinePage: '/offline/'};
Egy offline kép. Az SVG forrását Jeremy Keithnek köszönhetjük. (Nagy verzió megtekintése)

Vigyázzunk a CDN-ekkel

Vigyázzunk a CDN-ekkel, ha a lekérdezés kezelését az eredetire korlátozzuk. Az első szervizmunkásom felépítésekor elfelejtettem, hogy a tárhelyszolgáltatóm az eszközöket (képeket és szkripteket) a saját CDN-jébe osztotta, így azok már nem a webhelyem eredetéről (lyza.com) lettek kiszolgálva. Hoppá! Ez nem működött. Végül letiltottam a CDN-t az érintett eszközök számára (de természetesen optimalizáltam ezeket az eszközöket!).

Az első verzió befejezése

A szervizmunkásunk első verziója elkészült. Van egy install kezelőnk és egy kibővített fetch kezelőnk, amely az alkalmazható lehívásokra optimalizált válaszokkal tud válaszolni, valamint offline állapotban cache-elt erőforrásokat és offline oldalt biztosít.

Amint a felhasználók böngészik a webhelyet, egyre több cache-elt elemet fognak felhalmozni. Offline állapotban továbbra is böngészhetnek a már gyorsítótárazott elemek között, vagy egy offline oldalt (vagy képet) láthatnak, ha a kért erőforrás nem érhető el a gyorsítótárban.

A Chrome-ban tesztelheti, hogyan viselkedik offline a szolgáltatásmunkás, ha belép az “eszköz módba”, és kiválasztja az “Offline” hálózati előbeállítást. Ez egy felbecsülhetetlen értékű trükk. (Nagy verzió megtekintése)

A teljes kód a fetch-kezeléssel (serviceWorker.js) a GitHubon található.

Versioning And Updating The Service Worker

Ha soha többé nem változna semmi a weboldalunkon, mondhatnánk, hogy készen vagyunk. A szervizmunkásokat azonban időről időre frissíteni kell. Lehet, hogy több cache-elhető útvonalat akarok majd hozzáadni. Lehet, hogy fejleszteni akarom az offline visszaesések működését. Talán van valami kis hiba a szervizmunkásomban, amit ki akarok javítani.

Kiemelném, hogy léteznek automatizált eszközök, amelyekkel a szervizmunkások kezelése a munkafolyamat részévé tehető, mint például a Service Worker Precache a Google-től. Nem kell kézzel kezelni ezt a verziókezelést. Az én weboldalamon azonban a komplexitás elég alacsony ahhoz, hogy emberi verziókezelési stratégiát használjak a szervizmunkásom módosításainak kezelésére. Ez a következőkből áll:

  • egy egyszerű verziósztring a verziók jelzésére,
  • egy activate kezelő megvalósítása a régi verziók utáni takarításhoz,
  • a install kezelő frissítése a frissített szervizmunkások activate gyorsabbá tételéhez.

Versioning Cache Keys

Egy version tulajdonságot adhatok hozzá a config objektumomhoz:

version: 'aether'

Ez minden alkalommal változzon, amikor a szolgáltatásmunkásom frissített verzióját akarom telepíteni. Azért használom a görög istenségek neveit, mert számomra érdekesebbek, mint a véletlenszerű karakterláncok vagy számok.

Megjegyzés: Végeztem néhány változtatást a kódon, hozzáadtam egy kényelmi függvényt (cacheName) az előtaggal ellátott gyorsítótárkulcsok létrehozásához. Ez érintőleges, ezért ide nem teszem be, de az elkészült service worker kódban láthatod.

A Chrome-ban a “Resources” fülön láthatod a gyorsítótárak tartalmát. Láthatod, hogy a service workerem különböző verzióinak különböző cache nevei vannak. (Ez a achilles verzió.) (Nagy verzió megtekintése)

Ne nevezze át a szervizmunkást

Egy időben a szervizmunkás fájlnevének elnevezési konvencióival bíbelődtem. Ne tegye ezt. Ha megteszed, a böngésző regisztrálni fogja az új szervizmunkást, de a régi szervizmunkás is telepítve marad. Ez egy zűrös állapot. Biztos vagyok benne, hogy van megoldás, de én azt mondanám, hogy ne nevezze át a szervizmunkást.

Ne használjon importScripts for config

Elmentem azon az úton, hogy a config objektumomat egy külső fájlba helyeztem, és a self.importScripts()-t használtam a szervizmunkás fájlban a szkript behúzásához. Ez ésszerű módnak tűnt arra, hogy a config-emet a szervizmunkáson kívül kezeljem, de volt egy bökkenő.

A böngésző byte-összehasonlítja a szervizmunkás fájlokat, hogy megállapítsa, frissültek-e – így tudja, mikor kell újraindítani a letöltési és telepítési ciklust. A külső config változásai nem okoznak semmilyen változást magán a szervizmunkáson, ami azt jelenti, hogy a config változásai nem okozták a szervizmunkás frissítését. Hoppá.

Az aktiváló kezelő hozzáadása

A verzió-specifikus gyorsítótárnevek célja az, hogy a korábbi verziók gyorsítótárát ki tudjuk tisztítani. Ha az aktiválás során olyan gyorsítótárak vannak körülöttünk, amelyeknek nincs az aktuális verziószöveg előtagja, akkor tudni fogjuk, hogy törölni kell őket, mert gagyik.

A régi gyorsítótárak eltakarítása

A régi gyorsítótárak utáni takarításra használhatunk egy függvényt:

function onActivate (event, opts) { return caches.keys() .then(cacheKeys => { var oldCacheKeys = cacheKeys.filter(key => key.indexOf(opts.version) !== 0 ); var deletePromises = oldCacheKeys.map(oldKey => caches.delete(oldKey)); return Promise.all(deletePromises); });}

A telepítés és aktiválás felgyorsítása

A frissített szolgáltatásmunkás letöltődik és install a háttérben. Ez most már egy várakozó munkás. Alapértelmezés szerint a frissített szervizmunkás nem aktiválódik, amíg olyan oldalak töltődnek be, amelyek még mindig a régi szervizmunkást használják. Ezt azonban felgyorsíthatjuk a install kezelőnk egy kis módosításával:

self.addEventListener('install', event => { // … as before event.waitUntil( onInstall(event, config) .then( () => self.skipWaiting() ) );});

skipWaiting hatására activate azonnal megtörténik.

Most fejezzük be a activate kezelőt:

self.addEventListener('activate', event => { function onActivate (event, opts) { // … as above } event.waitUntil( onActivate(event, config) .then( () => self.clients.claim() ) );});

self.clients.claim az új szervizmunkás azonnal hatályba lép a hatókörébe tartozó minden megnyitott oldalon.

A Chrome-ban a chrome://serviceworker-internals speciális URL-t használva láthatjuk a böngésző által regisztrált összes szervizmunkást. (Nagy verzió megtekintése)
Itt látható a weboldalam, ahogyan a Chrome eszköz üzemmódjában megjelenik, az “Offline hálózat” előbeállítással, ami azt emulálja, amit a felhasználó látna, amikor offline állapotban van. Működik! (Nagy verzió megtekintése)

Ta-Da!

Már van egy verziókezelt szolgáltatási munkásunk! A verziókezeléssel frissített serviceWorker.js fájlt a GitHubon láthatod.

További olvasnivalók a SmashingMag-on:

  • A Beginner’s Guide To Progressive Web Apps
  • Building A Simple Cross-Browser Offline To-Do List
  • World Wide Web, Not Wealthy Western Web
(jb, ml, al, mse)

Vélemény, hozzászólás?

Az e-mail-címet nem tesszük közzé.