Making A Service Worker: A Case Study

About The Author

Lyza Danger Gardner on dev. Siitä lähtien, kun hän oli mukana perustamassa Portlandissa, Oregonissa sijaitsevaa mobiiliverkkoalan startup-yritystä Cloud Fouria vuonna 2007, hän on kiduttanut ja innostanut itseään …LisätietojaLyza↬

  • 23 min. luettu
  • Koodaus,JavaScript,Tekniikat,Palvelutyöntekijät
  • Tallennettu offline-lukemista varten
  • Jaa Twitterissä, LinkedIn
Tämässä artikkelissa kerrotaan, mikä on palvelutyöläinen ja miten voit koota itsellesi oman palvelutyökalun rekisteröimällä, asentamalla ja aktivoimalla sen vaivatta.

Korostusta ja innostusta ei puutu aloittelevasta service worker API:sta, jota nyt toimitetaan joissakin suosituissa selaimissa. On olemassa keittokirjoja ja blogikirjoituksia, koodinpätkiä ja työkaluja. Mutta minusta tuntuu, että kun haluan oppia uuden web-käsitteen perusteellisesti, on usein ihanteellista kääriä sananmukaiset hihat ylös, sukeltaa ja rakentaa jotakin tyhjästä.

Tällä kertaa kohtaamistani kolhuista ja kolhuista, ongelmista ja bugeista on hyötyä: Nyt ymmärrän palvelutyöntekijöitä paljon paremmin, ja hyvällä tuurilla voin auttaa sinua välttämään joitakin päänvaivoja, joita kohtasin työskennellessäni uuden API:n kanssa.

Palvelutyöntekijät tekevät paljon erilaisia asioita; on lukemattomia tapoja valjastaa niiden voimat. Päätin rakentaa yksinkertaisen palvelutyöntekijän (staattiselle, mutkattomalle) verkkosivustolleni, joka karkeasti ottaen peilaa ominaisuuksia, joita vanhentunut Application Cache API aikoinaan tarjosi – eli:

  • tehdä verkkosivusto toimivaksi offline-tilassa,
  • lisätä online-suorituskykyä vähentämällä verkkopyyntöjä tietyille omaisuuserille,
  • tarjota räätälöityä offline-varajärjestelyä.

Ennen kuin aloitan, haluaisin antaa tunnustusta kahdelle ihmiselle, joiden työ teki tämän mahdolliseksi. Ensinnäkin olen valtavan kiitollinen Jeremy Keithille palvelutyöntekijöiden toteuttamisesta hänen omalla verkkosivustollaan, joka toimi lähtökohtana omalle koodilleni. Sain inspiraatiota hänen äskettäisestä postauksestaan, jossa hän kuvailee meneillään olevia palvelutyöntekijäkokemuksiaan. Itse asiassa työni on niin vahvasti johdannainen, etten olisi kirjoittanut siitä ilman Jeremyn kehotusta aiemmassa postauksessa:

Jos siis päätät leikkiä Service Workereilla, ole kiltti, ole kiltti ja jaa kokemuksesi.

Toisekseen, kaikenlaiset suuret kiitokset Jake Archibaldille hänen erinomaisesta teknisestä tarkastuksestaan ja palautteestaan. Aina mukavaa, kun yksi Service Worker -spesifikaation luojista ja evankelistoista pystyy laittamaan asiat kuntoon!

Mikä on Service Worker?

Service Worker on skripti, joka asettuu verkkosivusi ja verkon väliin antaen sinulle muun muassa mahdollisuuden siepata verkon pyynnöt ja vastata niihin eri tavoin.

Voidaksesi verkkosivusi tai -sovelluksesi toimia selain noutaa sen omaisuuserät – kuten HTML-sivut, JavaScriptin, kuvat ja fontit. Aiemmin näiden hallinta oli pääasiassa selaimen etuoikeus. Jos selain ei päässyt verkkoon, näit todennäköisesti sen ”Hei, olet offline” -viestin. Oli tekniikoita, joita voit käyttää edistämään resurssien paikallista välimuistiin tallentamista, mutta selaimella oli usein viimeinen sana.

Tämä ei ollut kovin hyvä kokemus käyttäjille, jotka olivat offline-tilassa, ja se jätti web-kehittäjille vain vähän vaikutusvaltaa selaimen välimuistiin tallentamiseen.

Tulee sovellusvälimuistiin (Application Cache, AppCache), jonka tulo muutama vuosi sitten vaikutti lupaavalta. Sen avulla voit näennäisesti määrätä, miten eri aineistoja käsitellään, jotta verkkosivustosi tai sovelluksesi voisi toimia offline-tilassa. AppCachen yksinkertaisen näköinen syntaksi peitti kuitenkin sen taustalla olevan hämmentävän luonteen ja joustavuuden puutteen.

Kehitteillä oleva Service Worker API voi tehdä sen, mitä AppCache teki, ja paljon enemmänkin. Mutta se näyttää aluksi hieman pelottavalta. Spesifikaatiot ovat raskasta ja abstraktia luettavaa, ja lukuisat API:t ovat sen alaisuudessa tai liittyvät siihen muuten: cache, fetch jne. Palvelutyöntekijät kattavat niin paljon toimintoja: push-ilmoitukset ja pian myös taustasynkronointi. AppCacheen verrattuna se näyttää… monimutkaiselta.

Mikäli AppCache (joka muuten poistuu) oli helppo oppia, mutta kauhea joka ikinen hetki sen jälkeen (minun mielipiteeni), service workerit vaativat enemmän kognitiivisia alkuinvestointeja, mutta ne ovat tehokkaita ja hyödyllisiä, ja ne saa yleensä itse itsensä ulos ongelmista, jos rikkoo asioita.

Joitakin service workerin peruskonsepteja

Service worker on tiedosto, jossa on jonkin verran JavaScriptiä. Tuohon tiedostoon voit kirjoittaa JavaScriptiä niin kuin tiedät ja rakastat sitä, mutta muutama tärkeä asia on syytä pitää mielessä.

Palvelutyöntekijän skriptit toimivat selaimessa erillisessä säikeessä niiden hallitsemista sivuista. Työntekijöiden ja sivujen välillä on tapoja kommunikoida, mutta ne suoritetaan erillisessä laajuudessa. Tämä tarkoittaa, että sinulla ei ole pääsyä esimerkiksi näiden sivujen DOM:iin. Visualisoin service workerin ikään kuin suoritettavaksi erillisessä välilehdessä sivusta, johon se vaikuttaa; tämä ei ole lainkaan tarkka, mutta se on hyödyllinen karkea metafora, jolla pidän itseni poissa sekaannuksista.

JavaScript service workerissa ei saa estää. Sinun on käytettävä asynkronisia API:ita. Et voi esimerkiksi käyttää localStorage palvelutyöntekijässä (localStorage on synkroninen API). Humoristista kyllä, vaikka tiesin tämän, onnistuin rikkomaan sitä, kuten tulemme näkemään.

Palvelutyöntekijän rekisteröinti

Saat palvelutyöntekijän voimaan rekisteröimällä sen. Tämä rekisteröinti tehdään palvelutyöntekijän ulkopuolelta, jollakin toisella sivustosi sivulla tai skriptillä. Verkkosivustollani globaali site.js-skripti sisältyy jokaiseen HTML-sivuun. Rekisteröin palveluntyöläiseni sieltä.

Kun rekisteröit palveluntyöläisen, kerrot sille (valinnaisesti) myös, missä laajuudessa sen tulisi toimia. Voit määrätä palvelutyöntekijän käsittelemään asioita vain osassa sivustoa (esimerkiksi '/blog/') tai voit rekisteröidä sen koko sivustoa varten ('/'), kuten minä teen.

Palvelutyöntekijän elinkaari ja tapahtumat

Palvelutyöntekijä tekee suurimman osan työstään kuuntelemalla asiaankuuluvia tapahtumia ja reagoimalla niihin hyödyllisellä tavalla. Erilaiset tapahtumat käynnistyvät palvelutyöntekijän elinkaaren eri vaiheissa.

Kun palvelutyöntekijä on rekisteröity ja ladattu, se asennetaan taustalle. Palvelutyöntekijäsi voi kuunnella install-tapahtumaa ja suorittaa tähän vaiheeseen sopivia tehtäviä.

Tapauksessamme haluamme hyödyntää install-tilaa, jotta voimme esivarastoida joukon resursseja, jotka tiedämme haluavamme myöhemmin saataville offline-tilassa.

Vaiheen install päätyttyä palvelutyöntekijä aktivoidaan. Tämä tarkoittaa, että palvelutyöntekijä hallitsee nyt asioita scope:ssä ja voi tehdä asiansa. activate-tapahtuma ei ole kovin jännittävä uuden palvelutyöläisen kohdalla, mutta näemme, miten se on hyödyllinen päivitettäessä palvelutyöläistä uudella versiolla.

Tarkka aktivointihetki riippuu siitä, onko kyseessä aivan uusi palvelutyöläinen vai päivitetty versio jo olemassa olevasta palvelutyöläisestä. Jos selaimella ei ole jo rekisteröity aiempaa versiota kyseisestä palveluntyöläisestä, aktivointi tapahtuu heti asennuksen päätyttyä.

Kun asennus ja aktivointi on suoritettu, ne tapahtuvat uudelleen vasta, kun palveluntyöläisen päivitetty versio on ladattu ja rekisteröity.

Asennuksen ja aktivoinnin lisäksi tarkastelemme tänään ensisijaisesti fetch-tapahtumaa, jonka avulla palveluntyöläisemme voidaan tehdä hyödylliseksi. Sen lisäksi on kuitenkin useita hyödyllisiä tapahtumia: esimerkiksi synkronointitapahtumia ja ilmoitustapahtumia.

Lisäpisteinä tai vapaa-ajan huvin vuoksi voit lukea lisää rajapinnoista, jotka palvelutyöntekijät toteuttavat. Nämä rajapinnat toteuttamalla palvelutyöläiset saavat suurimman osan tapahtumista ja suuren osan laajennetusta toiminnallisuudestaan.

Palvelutyöläisen lupauksiin perustuva API

Palvelutyöläisen API käyttää paljon Promises. Lupaus edustaa asynkronisen operaation mahdollista tulosta, vaikka todellinen arvo ei tiedetä ennen kuin operaatio päättyy joskus tulevaisuudessa.

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

getAnAnswer…-funktio getAnAnswer… palauttaa Promise:n, joka (toivottavasti) lopulta täyttyy tai ratkaisee etsimämme answer:n. Sitten tuo answer voidaan syöttää mille tahansa ketjutetuille then käsittelijätoiminnoille, tai surullisessa tapauksessa, jos tavoite ei toteudu, Promise voidaan hylätä – usein syyn kera – ja catch käsittelijätoiminnot voivat huolehtia näistä tilanteista.

Lupauksia on enemmänkin, mutta yritän pitää esimerkit tässä suoraviivaisina (tai ainakin kommentoituina). Kehotan sinua tekemään informatiivista lukemista, jos lupaukset ovat sinulle uusia.

Huomautus: Käytän tiettyjä ECMAScript6:n (tai ES2015:n) ominaisuuksia palvelutyöntekijöiden esimerkkikoodissa, koska palvelutyöntekijöitä tukevat selaimet tukevat myös näitä ominaisuuksia. Erityisesti tässä käytän nuolifunktioita ja mallimerkkijonoja.

Muut palvelutyöläisten tarpeet

Huomaa myös, että palvelutyöläiset vaativat toimiakseen HTTPS:n. Tähän sääntöön on tärkeä ja hyödyllinen poikkeus: Tämä on helpotus, koska paikallisen SSL:n määrittäminen on joskus hankalaa.

Hauska fakta: Tämä projekti pakotti minut tekemään jotain, mitä olin lykännyt jo jonkin aikaa: hankkimaan ja määrittämään SSL:n verkkosivustoni www-alidomainille. Kehotan ihmisiä harkitsemaan tämän tekemistä, koska melkein kaikki hauskat uudet asiat, jotka tulevat selaimiin tulevaisuudessa, vaativat SSL:n käyttöä.

Kaikki nämä asiat, jotka kokoamme yhteen, toimivat tänään Chromessa (käytän versiota 47). Milloin tahansa Firefox 44 ilmestyy, ja se tukee palvelutyöntekijöitä. Is Service Worker Ready? tarjoaa yksityiskohtaista tietoa tuesta eri selaimissa.

Palvelutyöntekijän rekisteröinti, asennus ja aktivointi

Nyt kun olemme hoitaneet hieman teoriaa, voimme alkaa koota palvelutyöntekijäämme.

Palvelutyöntekijämme asentamiseksi ja aktivoimiseksi haluamme kuunnella install– ja activate-tapahtumia ja toimia niiden mukaan.

Voit aloittaa palvelutyöntekijämme tyhjällä tiedostolla ja lisätä pari eventListeners. Kohdassa serviceWorker.js:

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

Palvelutyöntekijämme rekisteröinti

Jatkossa meidän on kerrottava verkkosivujemme sivuille, että ne käyttävät palvelutyöntekijää.

Muista, että tämä rekisteröinti tapahtuu palvelutyöntekijän ulkopuolelta – minun tapauksessani skriptin (/js/site.js) sisältä, joka on sisällytetty verkkosivujeni jokaiselle sivulle.

Minussa site.js:

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

Staattisten objektien esivarastointi asennuksen aikana

Haluan käyttää asennusvaihetta joidenkin objektien esivarastointiin verkkosivustollani.

  • Varastoimalla valmiiksi joitakin staattisia omaisuuseriä (kuvia, CSS:ää, JavaScriptiä), joita monet sivustoni sivut käyttävät, voin nopeuttaa latautumisaikoja hakemalla ne välimuistista sen sijaan, että ne haettaisiin verkosta seuraavien sivulatausten yhteydessä.
  • Varastoimalla valmiiksi offline-varaussivun voin näyttää mukavan sivun, kun en voi täyttää sivupyyntöä, koska käyttäjä ei ole verkossa.

Vaiheet tähän ovat:

  1. Käsken install-tapahtuman jäädä odottamaan, eikä sitä saa suorittaa ennen kuin olen tehnyt sen, mitä minun täytyy tehdä, käyttämällä event.waitUntil.
  2. Avaa sopiva cache ja liitä siihen staattiset varat käyttämällä Cache.addAll. Progressiivisen web-sovelluksen kielenkäytössä nämä omaisuuserät muodostavat ”sovellukseni kuoren.”

Laajennetaan /serviceWorker.js:ssä install käsittelijää:

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

Palvelutyöntekijä toteuttaa CacheStorage-rajapinnan, joka tekee caches-ominaisuuden globaalisti saataville palvelutyöntekijässämme. caches:ssa on useita hyödyllisiä metodeja – esimerkiksi open ja delete.

Voit nähdä Promises:n toiminnassa tässä: caches.open palauttaa Promise:n, joka ratkaisee cache-olion, kun se on onnistuneesti avannut static-välimuistin; addAll palauttaa myös Promise:n, joka ratkaisee cache-olion, kun kaikki sille välitetyt kohteet on kätketty välimuistiin.

Käsken event:n odottamaan, kunnes käsittelijän funktioni palauttama Promise on ratkaistu onnistuneesti. Silloin voimme olla varmoja, että kaikki nuo välimuistissa olevat esineet saadaan lajiteltua ennen asennuksen päättymistä.

Konsolin sekaannukset

Taantunut lokitus

Ei ehkä ole bugi, mutta varmasti sekaannus: Jos console.log palvelutyöntekijöistä, Chrome jatkaa näiden lokiviestien näyttämistä uudelleen (eikä poista niitä) seuraavilla sivupyynnöillä. Tämä voi saada sen näyttämään siltä, että tapahtumat laukeavat liian monta kertaa tai että koodi suoritetaan yhä uudelleen ja uudelleen.

Lisätään esimerkiksi log-lause install-käsittelijäämme:

self.addEventListener('install', event => { // … as before console.log('installing');});
Cromen 47. versiosta lähtien ”asentava” lokiviesti jatkuu näkyviin seuraavissa sivupyynnöissä. Chrome ei todellakaan laukaise install-tapahtumaa jokaisella sivulatauksella. Sen sijaan se näyttää vanhentuneet lokitiedot. (Näytä suuri versio)

Virhe, kun asiat ovat kunnossa

Toinen outo asia on se, että kun palveluntyöläinen on asennettu ja aktivoitu, sen toimialueeseen kuuluvien sivujen myöhemmät sivulataukset aiheuttavat aina yhden virheen konsoliin. Luulin tekeväni jotain väärin.

Chrome 47:stä lähtien sivun käyttäminen jo rekisteröidyn service workerin kanssa aiheuttaa aina tämän virheen konsoliin. (Näytä suuri versio)

Mitä olemme saavuttaneet tähän mennessä

Palvelutyöntekijä käsittelee install-tapahtuman ja tallentaa valmiiksi välimuistiin joitakin staattisia aineistoja. Jos käyttäisit tätä palvelutyöntekijää ja rekisteröisit sen, se tosiaan esivarastoisi ilmoitetut omaisuuserät, mutta ei pystyisi vielä hyödyntämään niitä offline-tilassa.

Sisältö serviceWorker.js on GitHubissa.

Noutojen käsittely palvelutyöntekijöillä

Tässä vaiheessa palvelutyöntekijällämme on lihavoitu install-käsittelijä, mutta se ei tee mitään muuta. Palvelutyöntekijämme taika tulee todella tapahtumaan, kun fetch-tapahtumat käynnistyvät.

Voimme reagoida noutoihin eri tavoin. Käyttämällä erilaisia verkkostrategioita voimme kertoa selaimelle, että se yrittää aina noutaa tietyt aineistot verkosta (varmistaen, että keskeinen sisältö on tuoretta), ja samalla suosia välimuistiin tallennettuja kopioita staattisten aineistojen kohdalla, mikä todella pienentää sivun hyötykuormaa. Voimme myös tarjota mukavan offline-vaihtoehdon, jos kaikki muu epäonnistuu.

Kun selain haluaa noutaa assetin, joka on tämän palveluntyöläisen toimialueella, voimme kuulla siitä lisäämällä eventListener kohtaan serviceWorker.js:

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

Jälleen kerran jokainen nouto, joka kuuluu tämän palveluntyöläisen toimialueeseen (eli polkuun), laukaisee tämän tapahtuman – HTML-sivut, komentosarjat, skriptit, kuvat, CSS:t, mitä tahansa. Voimme valikoivasti käsitellä tapaa, jolla selain reagoi mihin tahansa näistä noudoista.

Should We Handle This Fetch?

Kun jollekin resurssille tapahtuu fetch-tapahtuma, haluan ensimmäiseksi määrittää, pitäisikö tämän palvelutyöntekijän puuttua kyseisen resurssin noutoon. Muussa tapauksessa sen ei pitäisi tehdä mitään ja antaa selaimen vakuuttaa oletuskäyttäytymisensä.

Päädymme seuraavanlaiseen peruslogiikkaan serviceWorker.js:

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); }});

Funktio shouldHandleFetch arvioi annettua pyyntöä määrittääkseen, pitäisikö meidän antaa vastaus vai antaa selaimen vakuuttaa oletuskäsittelynsä.

Miksi emme käytä lupauksia?

Pitäytyäkseni service workerin mieltymyksessä lupauksiin, ensimmäinen versio fetch-tapahtumankäsittelijästäni näytti tältä:

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

Tuntuu loogiselta, mutta tein pari aloittelijan virhettä lupausten kanssa. Vannon, että aistin koodin hajua jo aluksi, mutta Jake oli se, joka selvitti minulle virheeni. (Oppitunti: Kuten aina, jos koodi tuntuu väärältä, se todennäköisesti on sitä.)

Lupausten hylkäämistä ei pitäisi käyttää osoittamaan: ”Sain vastauksen, josta en pitänyt”. Sen sijaan hylkäysten tulisi osoittaa: ”Ah, paskat, jotain meni pieleen, kun yritin saada vastauksen”. Eli hylkäysten pitäisi olla poikkeuksellisia.

Kelpoisten pyyntöjen kriteerit

Oikein, takaisin sen määrittämiseen, soveltuuko tietty noutopyyntö palvelutyöntekijälleni. Sivustokohtaiset kriteerini ovat seuraavat:

  1. Pyydetyn URL-osoitteen tulisi edustaa jotakin, jonka haluan välimuistiin tai johon haluan vastata. Sen polun pitäisi sopia Regular Expression kelvollisten polkujen joukkoon.
  2. Pyynnön HTTP-metodin pitäisi olla GET.
  3. Pyynnön pitäisi kohdistua resurssiin, joka on peräisin alkuperäisestä palvelimestani (lyza.com).

Jos mikä tahansa criteria testeistä arvioi tulokseksi false, emme käsittele tätä pyyntöä. Kohdassa 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;}

Kriteerit tässä ovat tietysti omia kriteerejäni ja vaihtelisivat sivustoittain. event.request on Request-olio, jossa on kaikenlaista dataa, jota voit tarkastella arvioidaksesi, miten haluaisit noutokäsittelijäsi käyttäytyvän.

Triviaali huomautus: Jos huomasit config:n, joka välitetään opts:na käsittelijän funktioille, tunkeutumisen, hyvin huomattu. Karsin pois joitakin uudelleenkäytettäviä config:n kaltaisia arvoja ja loin config-olion palvelutyöntekijän ylimmän tason scopeen:

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

Miksi valkoluetteloon?

Voit ehkä ihmetellä, miksi välimuistiin tallennetaan vain sellaisia asioita, joiden polut vastaavat tätä säännöllistä lauseketta:

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

………………………………………………………………….. Pari syytä:

  • En halua välimuistiin itse palvelutyöntekijää.
  • Kun kehitän sivustoa paikallisesti, osa syntyvistä pyynnöistä koskee asioita, joita en halua välimuistiin. Käytän esimerkiksi browserSync, joka käynnistää joukon asiaan liittyviä pyyntöjä kehitysympäristössäni. En halua tallentaa niitä välimuistiin! Vaikutti sotkuiselta ja haastavalta yrittää miettiä kaikkea, mitä en haluaisi välimuistiin (puhumattakaan siitä, että oli hieman outoa kirjoittaa se palvelutyöntekijän konfiguraatiossa). Niinpä whitelist-lähestymistapa tuntui luonnollisemmalta.

Noutokäsittelijän kirjoittaminen

Nyt olemme valmiita välittämään sovellettavat fetch pyynnöt käsittelijälle. onFetchfunktion on määritettävä:

  1. minkälaista resurssia pyydetään,
  2. ja miten minun pitäisi täyttää tämä pyyntö.

1. Millaista resurssia pyydetään?

Voin katsoa HTTP Accept-otsikkoa saadakseni vihjeen siitä, millaista resurssia pyydetään. Tämä auttaa minua miettimään, miten haluan käsitellä sitä.

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}

Pysyäkseni järjestyksessä haluan laittaa erityyppisiä resursseja eri välimuisteihin. Näin voin hallita näitä kätköjä myöhemmin. Nämä välimuistien avaimet String ovat mielivaltaisia – voit kutsua välimuistejasi miksi haluat; välimuistien API:lla ei ole mielipiteitä.

2. Vastaa noutoon

Seuraavaksi onFetch on respondTo fetch-tapahtuman respondTo älykäs Response.

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)) ); }}

Varovasti asynkronisen kanssa!

Tapauksessamme shouldHandleFetch ei tee mitään asynkronista, eikä onFetch myöskään event.respondWith asti. Jos jotain asynkronista olisi tapahtunut ennen sitä, olisimme pulassa. event.respondWith on kutsuttava fetch-tapahtuman laukeamisen ja selaimelle palautuvan kontrollin välillä. Sama pätee myös event.waitUntil:een. Periaatteessa, jos käsittelet tapahtumaa, joko tee jotain välittömästi (synkronisesti) tai pyydä selainta odottamaan, kunnes asynkroniset asiasi on tehty.

HTML Content: Implementing A Network-First Strategy

fetch-pyyntöihin vastaaminen edellyttää sopivan verkkostrategian toteuttamista. Tarkastellaan tarkemmin tapaa, jolla vastaamme HTML-sisältöä (resourceType === 'content') koskeviin pyyntöihin.

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)) );}

Tapa, jolla tässä täytämme sisältöä koskevat pyynnöt, on verkko ensin -strategia. Koska HTML-sisältö on verkkosivustoni ydinasia ja se muuttuu usein, pyrin aina saamaan tuoreet HTML-dokumentit verkosta.

Käsitellään tätä vaiheittain.

1. Kokeile noutoa verkosta

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

Jos verkkopyyntö onnistuu (eli lupaus ratkeaa), siirry eteenpäin ja piilota kopio HTML-dokumentista sopivaan välimuistiin (content). Tätä kutsutaan läpi luettavaksi välimuistiksi:

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

Vastauksia saa käyttää vain kerran.

Meidän on tehtävä kaksi asiaa response-objektilla, joka meillä on:

  • kätköilemällä se,
  • vastaamalla sillä tapahtumaan (eli palauttamalla se).

Mutta Response-objekteja saa käyttää vain kerran. Kloonaamalla sen voimme luoda kopion välimuistin käyttöön:

var copy = response.clone();

Ei huonoja vastauksia saa tallentaa välimuistiin. Älä tee samaa virhettä kuin minä tein. Koodini ensimmäisessä versiossa ei ollut tätä ehtoa:

if (response.ok)

Aika mahtavaa päätyä 404- tai muihin huonoihin vastauksiin välimuistissa! Vain onnelliset vastaukset välimuistiin.

2. Yritä hakea välimuistista

Jos assetin hakeminen verkosta onnistuu, olemme valmiita. Jos se ei kuitenkaan onnistu, saatamme olla offline-tilassa tai muuten verkko-ongelmissa. Kokeile hakea HTML:n aiemmin välimuistiin tallennettu kopio välimuistista:

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

Tässä on fetchFromCache-funktio:

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; });}

Huomautus: Älä ilmoita, minkä välimuistin haluat tarkistaa caches.match:llä; tarkista ne kaikki kerralla.

3. Provide an Offline Fallback

Jos olemme päässeet näin pitkälle, mutta välimuistissa ei ole mitään, mihin voisimme vastata, palauta sopiva offline fallback, jos mahdollista. HTML-sivujen osalta tämä on /offline/ välimuistissa oleva sivu. Se on kohtuullisen hyvin muotoiltu sivu, joka kertoo käyttäjälle, että hän on offline-tilassa ja että emme voi täyttää sitä, mitä hän haluaa.

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

Ja tässä on offlineResponse-toiminto:

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;}
Offline-sivu (Näytä suuri versio)

Muut resurssit: Cache-First-strategian toteuttaminen

Muiden resurssien kuin HTML-sisällön noutologiikka käyttää cache-first-strategiaa. Kuvat ja muu sivuston staattinen sisältö muuttuvat harvoin, joten tarkista ensin välimuisti ja vältä verkon kiertomatka.

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

Vaiheet ovat seuraavat:

  1. Yritä hakea resurssi välimuistista;
  2. jos se ei onnistu, yritä hakea verkosta (lukuvälimuistitallennuksella);
  3. jos se ei onnistu, tarjoa offline-vararesurssi (offline fallback-resurssi), jos se on mahdollista.

Offline-kuva

Voidaan palauttaa SVG-kuva, jossa on teksti ”Offline”, offline fallbackina täydentämällä offlineResource-funktio:

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

Tehdään asiaankuuluvat päivitykset config:

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/'};

Offline-kuva. Kiitos SVG-lähteestä kuuluu Jeremy Keithille. (Näytä suuri versio)

Varo CDN:iä

Varo CDN:iä, jos rajoitat noutokäsittelyn alkuperään. Rakentaessani ensimmäistä palvelutyöntekijääni unohdin, että hosting-palveluntarjoajani hajautti omaisuuseriä (kuvia ja skriptejä) CDN:äänsä, joten niitä ei enää tarjoiltu verkkosivustoni alkuperästä (lyza.com). Hups! Se ei toiminut. Päädyin poistamaan CDN:n käytöstä asianomaisten omaisuuserien osalta (mutta optimoimalla nämä omaisuuserät tietysti!).

Ensimmäisen version valmistuminen

Palvelutyöntekijämme ensimmäinen versio on nyt valmis. Meillä on install-käsittelijä ja täytetty fetch-käsittelijä, jotka voivat vastata sovellettaviin noutoihin optimoiduilla vastauksilla sekä tarjota välimuistiin tallennettuja resursseja ja offline-sivun offline-tilanteessa.

Käyttäjien selatessa verkkosivustoa he keräävät jatkuvasti lisää välimuistiin tallennettuja kohteita. Kun he ovat offline-tilassa, he voivat jatkaa jo välimuistissa olevien kohteiden selaamista, tai he näkevät offline-sivun (tai kuvan), jos pyydettyä resurssia ei ole välimuistissa.

Chromessa voit testata, miten palveluntyöläisesi käyttäytyy offline-tilassa, siirtymällä ”laitetilaan” ja valitsemalla verkkoasetukseksi ”Offline”. Tämä on korvaamaton temppu. (Näytä suuri versio)

Koko koodi noutokäsittelyineen (serviceWorker.js) on GitHubissa.

Palvelutyöntekijän versioiminen ja päivittäminen

Jos mikään ei enää koskaan muuttuisi sivustollamme, voisimme sanoa, että olemme valmiita. Palvelutyöntekijöitä on kuitenkin aika ajoin päivitettävä. Ehkä haluan lisätä lisää välimuistiin tallennettavia polkuja. Ehkä haluan kehittää tapaa, jolla offline-pudotuspalveluni toimivat. Ehkä palvelutyöntekijässäni on jotain hieman bugista, jonka haluan korjata.

Haluan korostaa, että on olemassa automatisoituja työkaluja, joilla palvelutyöntekijöiden hallinnasta voi tehdä osan työnkulkua, kuten Service Worker Precache Googlelta. Sinun ei tarvitse hallita versiointia tätä käsin. Sivustoni monimutkaisuus on kuitenkin sen verran vähäistä, että käytän inhimillistä versiointistrategiaa palvelutyöntekijän muutosten hallintaan. Tämä koostuu:

  • yksinkertaisesta versio-merkkijonosta versioiden ilmoittamiseksi,
  • toteutuksesta activatekäsittelijän siivoamiseksi vanhojen versioiden jälkeen,
  • käsittelijän install päivittämisestä, jotta päivitetyt palvelutyöläiset olisivat activate nopeampia.

Versioinnin kätköavaimet

Voin lisätä config-objektiin versionominaisuuden:

version: 'aether'

Tämän pitäisi muuttua aina, kun haluan ottaa käyttöön päivitetyn version palvelutyöntekijästäni. Käytän kreikkalaisten jumaluuksien nimiä, koska ne ovat minusta kiinnostavampia kuin satunnaiset merkkijonot tai numerot.

Huomautus: Tein joitakin muutoksia koodiin lisäämällä siihen kätevän funktion (cacheName), jolla voidaan generoida etuliitteellisiä välimuistiavaimia. Se on tangentiaalinen, joten en sisällytä sitä tähän, mutta näet sen valmiissa service worker -koodissa.

Chromessa näet välimuistien sisällön ”Resurssit”-välilehdellä. Näet, kuinka service workerini eri versioilla on erilaiset välimuistien nimet. (Tämä on versio achilles.) (Näytä suuri versio)

Älä nimeä palvelutyöntekijääsi uudelleen

Jossain vaiheessa pähkäilin palvelutyöntekijän tiedostonimen nimeämiskäytäntöjen kanssa. Älä tee näin. Jos teet niin, selain rekisteröi uuden palveluntyöläisen, mutta vanha palveluntyöläinen pysyy myös asennettuna. Tämä on sotkuinen tilanne. Olen varma, että on olemassa kiertotie, mutta sanoisin, että älä nimeä palvelutyöntekijääsi uudelleen.

Ei importScriptsin käyttöä configissa

Kävin polkua, jossa laitoin config-olion ulkoiseen tiedostoon ja käytin self.importScripts() palvelutyöntekijän tiedostossa self.importScripts()-oliota tuon skriptin vetämiseen sisään. Se vaikutti järkevältä tavalta hallita config-tiedostoani palvelutyöntekijän ulkopuolella, mutta siinä oli ongelma.

Selain vertailee palvelutyöntekijätiedostoja tavuilla määrittääkseen, onko ne päivitetty – näin se tietää, milloin lataus- ja asennussykli on käynnistettävä uudelleen. Ulkoisen config:n muutokset eivät aiheuta muutoksia itse palvelutyöntekijään, mikä tarkoittaa, että config:n muutokset eivät aiheuttaneet palvelutyöntekijän päivitystä. Hups.

Aktivaatiokäsittelijän lisääminen

Versiokohtaisten välimuistien nimien tarkoituksena on, että voimme siivota aiempien versioiden välimuistit. Jos aktivoinnin aikana on liikkeellä kätköjä, joiden etuliitteenä ei ole nykyisen version merkkijonoa, tiedämme, että ne pitäisi poistaa, koska ne ovat rapeita.

Vanhojen kätköjen siivoaminen

Voimme käyttää funktiota vanhojen kätköjen jälkeisten kätköjen siivoamiseen:

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); });}

Asennuksen ja aktivoimisen nopeuttaminen

Päivitetty palveluntyöläinen ladataan ja se installtyöstetään

taustalla. Se on nyt odottava työntekijä. Oletusarvoisesti päivitetty palvelutyöntekijä ei aktivoidu, kun ladataan sivuja, jotka käyttävät vielä vanhaa palvelutyöntekijää. Voimme kuitenkin nopeuttaa tätä tekemällä pienen muutoksen install-käsittelijäämme:

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

skipWaiting saa aikaan sen, että activate tapahtuu välittömästi.

Nyt viimeistelemme activate-käsittelijän:

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

self.clients.claim saa uuden palvelutyöläisen tulemaan voimaan välittömästi kaikkiin sen vaikutuspiirissä oleviin avoinna oleviin sivuihin.

Käyttäessäsi Chromessa erityistä URL-osoitetta chrome://serviceworker-internals voit nähdä kaikki selaimen rekisteröimät palvelutyöläiset. (Näytä suuri versio)
Tässä on verkkosivustoni sellaisena kuin se näkyy Chromen laitetilassa, jossa on esiasetus ”Offline-verkko”, joka jäljittelee sitä, mitä käyttäjä näkisi offline-tilassa. Se toimii! (Katso suuri versio)

Ta-Da!

Meillä on nyt versiohallittu palvelutyöntekijä! Voit nähdä päivitetyn serviceWorker.js tiedoston versionhallinnalla GitHubissa.

Lisälukemista SmashingMagissa:

  • 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)

Vastaa

Sähköpostiosoitettasi ei julkaista.