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
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:
- 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
. - 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');});
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.
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:
- Pyydetyn URL-osoitteen tulisi edustaa jotakin, jonka haluan välimuistiin tai johon haluan vastata. Sen polun pitäisi sopia
Regular Expression
kelvollisten polkujen joukkoon. - Pyynnön HTTP-metodin pitäisi olla
GET
. - 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. onFetch
funktion on määritettävä:
- minkälaista resurssia pyydetään,
- 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;}
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:
- Yritä hakea resurssi välimuistista;
- jos se ei onnistu, yritä hakea verkosta (lukuvälimuistitallennuksella);
- 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/'};
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.
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
activate
käsittelijän siivoamiseksi vanhojen versioiden jälkeen, - käsittelijän
install
päivittämisestä, jotta päivitetyt palvelutyöläiset olisivatactivate
nopeampia.
Versioinnin kätköavaimet
Voin lisätä config
-objektiin version
ominaisuuden:
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.
Ä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 install
työ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.
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