Pokud se vám zatím úspěšně dařilo vyhýbat se implementaci Service Workeru na svých stránkách, ale nyní potřebujete doplnit nějakou funkcionalitu, kterou nabízí, zde je přehledný popis pro začátečníky.
Co to je?
Service Worker (do češtiny by se dalo přeložit jako Pracovní služba, ale přesnější je Služba na pozadí) umožňuje stránce spouštět kód v pozadí a provádět akce, i když není stránka připojená k serveru (např. je uživatel bez internetu) nebo ani nemá otevřené okno v prohlížeči.
Service Worker (dále jen služba) se z vaší stránky nainstaluje do prohlížeče (podobně jako třeba rozšíření nebo plug-in) a prohlížeč si zjistí, na které události chce služba reagovat a pokud k takové události dojde, spustí příslušný kód služby.
Service Worker plně funguje pouze v Edge, Firefox a Chrome (a ostatních Chromium prohlížečích). Apple sice v Safari (WebKit) implementuje část specifikace Service Workeru (např. fetch), ale pro Push zprávy používá vlastní placenou službu Safari Push Notifications (MacOS; musíte si koupit licenci Apple Developer) a na iOS musíte mít vlastní aplikaci.
Co Service Worker dokáže?
Zobrazení v offline režimu
Služba může reagovat na událost fetch
, která se vyvolá kdykoliv vaše stránka požádá server o nějaký soubor (HTML, CSS, obrázek, atd.). Služba pak může soubor stáhnout sama a uložit si ho do cache. Pokud pak uživatel přejde do offline režimu a bude chtít znovu navštívit danou stránku, služba může daný soubor vrátit z cache a fungovat tedy i bez internetu.
Samozřejmě v offline režimu nemusíte zobrazovat přesně to samé jako při online prohlížení. Ve většině případů bude uživateli stačit nějaká zjednodušená verze stránky, která mu umožní provést ty nejdůležitější úkony. Např. offline verze Facebooku může uživateli dovolit napsat nový status a připravit fotky k nahrání, ale nebude mu zobrazovat příspěvky ostatních uživatelů.
Stahování a odesílání dat na pozadí
Sledováním událostí sync
a online
dokáže služba poznat, kdy uživatel přešel do online režimu a může například odeslat data, která uživatel uložil, když by offline, nebo může naopak stáhnout ze serveru aktualizace (např. nové články nebo zprávy z chatu) a uložit je do cache (odkud se pak mohou zobrazit v offline režimu).
Sledování PUSH notifikací
Služba může uživatele požádat o povolení k zasílání oznámení. Pokud uživatel oznámení povolí, může pak váš server odesílat zprávy přímo do počítače nebo zařízení daného uživatele bez toho, aby měl uživatel otevřenu vaši stránku nebo si musel stahovat speciální aplikaci.
V případě, že je uživatel zrovna offline, notifikace se uloží na serveru (výrobce jeho prohlížeče, např. Microsoft, Google, Mozzila, apod.) a zobrazí se hned, jak se uživatel připojí k internetu.
Služba může dále reagovat na to, když uživatel klikne na zobrazené upozornění a provést nějakou akci. Zpravidla otevře prohlížeč s příslušnou stránkou, ale také může jen na pozadí odeslat data na server nebo naopak stáhnout aktualizace ze serveru.
Stejně tak dokáže poznat, když uživatel upozornění zruší (zavře).
A nakonec služba může na základě dalších dat notifikaci upravit a přidat do ní tlačítka s různými akcemi. Například pokud zobrazíte notifikaci na zprávu od uživatele, můžete přidat akce „Odpovědět“, „Zobrazil profil“ a „Připomenou později“.
Odesílání zpráv do stránky
Nepleťte si to s upozorněními (notification) nebo chat zprávami (messenger).
Služba může odeslat zprávu, na kterou pak může reagovat propojená stránka, pokud je zrovna spuštěna. Například pokud služba na pozadí stáhne nové články nebo chat zprávy a uloží je do cache, může poslat zprávu „chat-updated
“ a stránka pak může zobrazit uživateli upozornění nebo zprávy rovnou z cache načíst.
Lazy-loading (zpožděné zobrazení)
Službu lze kombinací výše uvedených funkcí použít pro lazy-loading (například obrázků).
Služba zareaguje na požadavek na obrázek tím, že z cache vrátí nějaký výchozí obrázek nebo načítací animaci, následně spustí timer a po nějaké době obrázek stáhne ze serveru a uloží do cache. Pak stránce pošle zprávu s tím, že obrázky jsou již dostupné a stránka je buď vytáhne z cache nebo o ně znovu požádá server (což služba odchytí a sama je vrátí z cache).
Výhoda je v tom, že lazy-loading se provede jen poprvé a při dalším zobrazení se již budou obrázky zobrazovat ihned z cache.
Co Service Worker nedokáže?
Služba neběží nepřetržitě, takže nedokáže například sledovat uživatele (vyjma dále uvedených případů jako je přechod do offline režimu). Navíc běží ve vlastním javaskriptovém prostředí (ServiceWorkerGlobalScope
), takže nemá přístup k ostatním službám zařízení (např. nemůže sledovat hovory či zprávy)
stejně jako plnohodnotná aplikace .
Služba sice může sama sebe udržet v běhu po určitou dobu (řádově sekundy nebo minuty), ale nemůže běžet nepřetržitě několik hodin nebo dní – a to ani nastavením intervalu nebo timeoutu. Každý prohlížeč má nějaké maximum, po jakou dobu může služba běžet a na jak dlouho může nastavit timeout, a pokud služba toto maximum překročí, prohlížeč ji prostě ukončí (resp. nespustí časovač, pokud je příliš dlouhý).
Služba také nemůže nekonečně ukládat data do cache. Každý prohlížeč má nějaké maximum (5 až 50MB), po jejichž překročení buď zakáže zápis (Chrome a Safari) nebo upozorní uživatele (Edge a Firefox), že stránka využívá příliš místa na disku a požádá o povolení dalšího místa nebo smazání dat.
Služba běží v pozadí odděleně od vlastní stránky, takže i když je stránka otevřená, služba nedokáže (přímo) měnit její obsah (DOM), vyvolávat události (např. klik na tlačítko) ani číst a měnit proměnné vytvořené v globálním scope (window
). Služba a stránka ale spolu mohou komunikovat pomocí interních zpráv.
Navíc služba běží v rámci jednoho prohlížeče, takže pokud uživatel daný prohlížeč odinstaluje nebo převede do režimu spánku, služba přestane fungovat. Pokud uživatel prohlížeč resetuje nebo přeinstaluje, může být potřeba službu znovu nainstalovat a nastavit.
Importy
Service worker běží jako běžný skript, takže nemůže načítat funkce, proměnné a třídy pomocí klíčového slova import
. Není tedy možné sdílet části kódu mezi službou a moduly.
Co ale může udělat, je načíst a spustit kód z externího souboru pomocí self.importScripts('soubor.js')
. Pokud takový soubor definuje proměnné nebo funkce v globálním scope, budou vytvořeny v prostředí služby a ta je pak může používat.
Požadavky a testování
Aby mohl Service Worker fungovat, musí být vaše stránka bezpečná. To v praxi znamená, že musí být stažena přes HTTPS a mít platný certifikát nebo pro testování musí běžet na adrese http://localhost
(s libovolným portem). Ve Firefox Debugger je navíc checkbox „Povolit Service Worker přes HTTP“.
Pokud pro vývoj používáte lokální server s vlastní adresou (např. http://server.lc
), nebude možné službu nainstalovat a spustit (a to ani když server.lc
odkazuje na 127.0.0.1
).
Samozřejmě potřebujete prohlížeč se zapnutým javaskritem a podporou Service Worker a dalších služeb, které chcete využívat.
Pro použití Push notifikací musí váš server (resp. localhost) být schopen odesílat HTTPS požadavky na servery třetích stran (musí mít tedy správně nastavené např. CURL).
Registrace Service Workeru
Teorii máme za sebou a už víme, k čemu je Service Worker. Nyní se podíváme, jak ho přidat do vaší stránky.
Příklady od Mozzily
Skupina Mozilla (výrobce Firefoxu) připravila kuchařku serviceworke.rs, na které najdete spoustu příkladů, jak službu používat.
Web Worker
Specifikace a funkčnost Service Workeru vychází z existující funkčnosti Web Workeru, který je dostupný již od IE10 a umožňuje část Javascriptu spustit v novém vlákně (Thread) tak, že kód načtete ze samostatného souboru.
Pokud jste na Worker ještě nenarazili, doporučuji se seznámit s jeho základní myšlenkou. Bude vám pak jasnější, proč Service Worker funguje tak, jak je níže popsáno a proč používá některé zdánlivě nelogické postupy.
Formát dat
Kdykoliv v rámci Service Workeru posíláte nějaká data, vždy jde o volný formát, takže co a jak uložíte záleží na vás. Můžete poslat buď jednoduchý řetězec, číslo a nebo objekt. Pokud data posíláte z PHP, Javy, apod. je potřeba objekt přeložit do JSON.
Doporučuji ještě před implementací promyslet struktura JSON dat, pomocí které budete data posílat, aby byla univerzální a šla použít v různých situacích (příjem push notifikací, stažení dat ze serveru, posílání zpráv mezi stránkou a službou, atd.).
Podpora
Jako první krok musíme ověřit, že daný prohlížeč podporuje služby, které chcete použít.
Pro začátek zjistíme, jestli je podporovaný Service Worker jako takový:
if ('serviceWorker' in navigator) {
console.log('Služba je podporována');
}
Pokud chcete používat službu pro sledování offline režimu a ukládání souborů do cache, měli by to podporovat všechny prohlížeče s podporou Service Workeru.
Offline režim můžete sledovat jednoduše registrací události fetch
uvnitř služby. Pokud prohlížeč offline režim nepodporuje, prostě tuhle událost nevyvolá. Registraci události si ukážeme níže.
Podporu cache můžete ověřit přes třídu Cache
resp. její instanci caches
:
if ('Cache' in window OR 'caches' in window) {
console.log('Cache je podporována');
}
Pro použití služby PUSH notifikací musíte ověřit, že kromě Service Workeru podporuje prohlížeč i Push Manager, přes který můžete uživatele požádat o povolení. Také byste měli zkontrolovat, zda již v minulosti uživatel upozornění nezakázal, abyste zbytečně nezobrazovali nefunkční prvky.
if ('serviceWorker' in navigator) {
console.log('Služba je podporována');
if ('PushManager' in window) {
console.log('Push notifikace jsou podporovány');
if ('denied' === Notification.permission) {
console.log('Uživatel notifikace odmítl');
}
}
}
Hodnota Notification.permission
bude denied
, pokud uživatel výslovně odmítl notifikace nebo není možno notifikace zobrazit (např. web není bezpečný nebo jsou notifikace globálně zakázány zabezpečením zařízení nebo prohlížeče). Hodnota bude granted
, pokud již uživatel souhlasil se zasíláním notifikací z vaší stránky (tzn. již jste se ho dříve zeptali). Hodnota default
znamená, že můžete požádat uživatele o povolení a on ho buď schválí nebo zamítne.
Pro ověření, zda je možno odesílat zprávy ze služby do stránky, lze zkontrolovat vlastnost onmessage
:
if ('onmessage' in navigator.serviceWorker) {
console.log('Zprávy jsou podporovány');
}
Pro synchronizaci na pozadí potřebujete použít Sync Manager, který ale není zatím standardizován a tak nemusí být přítomen v prohlížečích, které podporují ostatní části Service Workeru. Pro kontrolu podpory použijte (po té, co službu nainstalujete):
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(reg => {
if ('sync' in reg) {
console.log('Synchronizace je podporována');
}
}
}
Služba je modul s vlastním scope
Uvnitř souboru Service Workeru můžete používat funkce ES6, protože Service Worker je jejich součástí. Také můžete používat klíčové slovo self
, které funguje stejně jako window
a odkazuje na globální namespace dané služby. Také můžete vytvářet globální proměnné přes var
, let
nebo const
a tyto budou dostupné pouze dané službě a nebudou kolidovat s proměnnými ostatních stránek.
Instalace služby
Pro použití služby musíte vytvořit samostatný soubor, který následně zaregistrujete jako službu:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('service.js');
}
Důležité je si uvědomit, že služba se může propojit pouze se stránkami ve své složce. Službu tedy nemůžete umístit do podsložky (např. /js/offline.js
), protože by se pak nemohla propojit s hlavní stránkou (např. /index.html
) a sledovat její requesty a zprávy.
Samozřejmě, pokud chcete mít soubory na serveru organizované, můžete vytvořit přesměrování (např. REWRITE RULE), které /offline.js
přesměruje na /js/service-worker/main.js
.
Alternativní možnost je nakonfigurovat web-server tak, aby společně se souborem služby poslal HTTP hlavičku Service-Worker-Allowed
, která bude obsahovat adresu, kterou chcete službě povolit:
#příklad pro Apache server
#soubor .htaccess ve složce /js/
Header add Service-Worker-Allowed "/"
//příklad pro PHP
<?php //skript pro stažení service.js
header('Service-Worker-Allowed=/');
readfile(WWW_DIR . '/js/service.js');
Pokud naopak chcete službu použít pouze pro sledování určitých požadavků (např. v podsložce blog), můžete při registraci zadat parametr scope
:
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service.js',
{ scope: './blog/' });
}
V tomto případě pak služba bude schopna propojit se pouze se stránkami v podložce blog (např. /blog/article/123
), ale už ne z jiných složek (např. /archive/article/123
).
Jedna stránka může zaregistrovat více služeb, ale registrace druhé služby se stejným scope
automaticky odregistruje tu předchozí. Více souběžně nainstalovaných služeb tedy můžete mít pouze v případě, že každá sleduje jiný scope
(např. jedna sleduje blog na scope: /blog/
a druhá archiv z scope: /archive/
) nebo jedna sleduje část scopu druhé (např. jedna sleduje vše v scope: /
) a druhá třeba jen archive (scope: /archive/
) – v tom případě pouze služba s více specifickým scope
může sledovat fetch
události z daného scope.
Důležité je uvědomit si, že v případě sledování fetch událostí rozhoduje scope stránky, která požadavek odeslala a nikoliv URL souboru nebo stránky, která se stahuje. Pokud například stránka /archive/article/123
stahuje soubor /article/img/123-a.jpg
, služba bude schopna fetch odchytit, pokud má scope /archive/article/
ale nikoliv pokud má scope /article/
! Pokud tedy chcete sledovat určitý typ souborů (např. obrázky, CSS styly, JS skripty, apod.) nemůžete scope nastavit na složku, kde máte tyto soubory uloženy (např. { scope: /img/ }
).
Aktualizace služby
Soubor služby je potřeba udržovat aktuální, aby bylo možno přidávat nové funkce a opravovat chyby.
K tomu prohlížeč nabízí několik možností:
- když prohlížeč přijme
push
nebosync
událost a aktuálně nainstalovaná služba je starší než 24 hodin, pokusí se ji automaticky aktualizovat ze serveru (z adresy zadané v.register()
metodě); - služba sama (nebo propojená stránka) může zavolat metodu
self.update()
; - k aktualizaci také dojde, pokud zavoláte
.register()
s jinou URL, ale stejnýmscope
parametrem. Pozor ale na to, že pokud stará služba vrací HTML stránku z cache, nemusí se nová služba z nové URL nainstalovat, protože v cache je stále uložena stará URL adresa. V tom případě by služba sama měla nějak kontrolovat verzi souboru a případně vymazat staré HTML z cache.
Kdykoliv se služba aktualizuje, ale tento proces selže, ať už proto že URL neexistuje (404), nejde zkompilovat (chybná syntaxe) nebo během instalace (tzn. v handleru události install
) služba vyhodí výjimku, tak zůstane aktivní stará služba.
Stará služba také zůstává aktivní do doby, než uživatel zavře všechna okna stránek obsahujících volání .register()
s danou URL. Nová služba může toto obejít zavoláním metody self.skipWaiting()
.
Stará služba může reagovat na událost updatefound
a získat přístup k nové verzi služby přes self.registration.installing
. U ní si pak třeba může zaregistrovat potřebné události (např. install
, viz dále) nebo jí třeba přes postMessage
poslat data.
Pokud naopak nová verze služby potřebuje poslat zprávu svému předchůdci, aby například smazal svojí cache, pokud obsahuje starší verzi, a uvolnil místo svému nástupci, můžete to provést přes self.registration.active.postMessage()
:
let version = 1; //aktualizujte když změníte službu
self.addEventListener('install', e => {
if (self.registration.active) {
e.waitUntil(self.registration
.active.postMessage({
update: version
})
);
}
});
self.addEventListener('message', e => {
if (e.message.hasOwnProperty('update')
&& version < e.message.update
) {
e.waitUntil(caches.delete('workerCache'));
}
});
V příkladu vidíte službu, která může aktualizovat sama sebe a poslat svému staršímu já zprávu, aby smazala existující cache: když služba instaluje novější verzi, ověří, zda existuje starší verze, a pokud ano, pošle jí zprávu o aktualizaci na novější verzi cache. Starší verze zjistí, že dostala zprávu s klíčem update
a pokud má nová služba vyšší verzi cache, (stará služba) smaže svoji cache. Díky použití e.waitUntil()
uvnitř message
handleru nedojde k dokončení instalace nové služby, dokud ta stará nevymaže celou cache.
Reakce na instalaci
Instalátor služby vrací Promise
, takže můžete spustit další kód v okamžiku, kdy se služba nainstaluje a prvně spustí. To odpovídá spuštění konstruktoru u tříd nebo události onCreate
:
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service.js')
.then(function(reg) {
console.log('Služba je nainstalována');
})
;
}
V souboru Service Workeru pak můžete reagovat na událost install
:
self.addEventListener('install', e => {
//další práce se službou
});
První parametr získaný v handleru register().then()
je instance třídy ServiceWorkerRegistration
(zkracuje se reg
) a obsahuje metody pro práci se službou (např. push notifikace, sync požadavky, atd.). Pro získání informací o aktuálně spuštěné službě použijte reg.active
, pro ověření existence novější verze reg.installing
(je NULL
pokud neexistuje).
Služba sama může používat proměnnou self
(instance třídy ServiceWorkerGlobalScope
; obdoba window
ve stránce) – všechna volání jako self.fetch()
nebo self.pushManager
je možno zkrátit vynecháním slova self
(stejně jako můžete ze stránky volat např. setTimeout()
nebo location
z objektu window
).
Registrační objekt obsahuje mimo jiné vlastnosti location
(URL, ze které se služba nainstalovala) a scope
(složka, ze které může služba sledovat fetch
požadavky)
Poznámka: v ES6 je doporučeno používat metodu self.addEventListener(event, () => {})
, ale i v Service Workeru stále můžete používat starý způsob s vlastnostmi onEvent = function() {}
(např. self.onInstall
, self.onActivate
, atd.).
To, že se služba nainstaluje, ještě neznamená, že začne plně fungovat. Po instalaci teprve prohlížeč analyzuje registrované události a teprve poté je služba schopna reagovat na události jako fetch
nebo sync
. Ve stejný okamžik se také vyvolá událost activate
:
self.addEventListener('activate', e =>
console.log('Služba je připravena pro fetch')
);
Událost activate
je možno z venku sledovat pomocí Promise
uloženým do vlastnosti ready
:
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('service.js');
navigator.serviceWorker.ready
.then(function(reg) {
console.log('Služba je připravena na fetch');
});
}
A dále pak to, že je služba aktivována, neznamená, že začne okamžitě hlídat requesty ze stránky, která ji aktivovala. Kvůli tomu, že instalace služby nějakou dobu trvá prohlížeče dělají to, že stránka, který službu nainstalovala ji nemůže používat. K prvnímu použití služby tak dojde po dalším načtení stránky (reload) nebo po přejití na jinou stránku (navigace).
Obejít se to dá tak, že si služba vynutí hlídání requestů ze všech právě otevřených oken prohlížeče (samozřejmě těch se stránkami z vaší domény. Provádí se to pomocí self.clients.claim()
obaleným voláním waitUntil()
, jelikož je asynchronní:
self.addEventListener('activate', e
=> e.waitUntil(clients.claim())
);
K aktivaci služby nedojde, pokud již v prohlížeči existuje (jiná nebo starší) služba hlídající stejný scope. V takovém případě dojde k aktivaci nové služby až po té, co se ukončí všechny stránky využívající starou službu.
Stav instalace a aktivace služby je možno sledovat z proměnné self.registration.active.state
, která obsahuje textové hodnoty installing
(během volání události install
), installed
(po skončení install
ale před vyvoláním activate
), activating
(během zpracování activate
) a activated
(po dokončení aktivace). Stav redundant
znamená, že služba byla nahrazena novější verzí a již nebude přijímat žádné události.
Hlídání Offline souborů
Pro hlídání stahování souborů a jejich odeslání při Offline režimu je potřeba zaregistrovat si událost fetch
v souboru služby.
self.addEventListener('fetch',
e => e.respondWith(
fetch(e.request)
.then(response => caches.open('offline')
then(cache => cache.put(response))
)
.catch(f => caches.open('offline')
.then(cache => cache
.match(e.request)
.then(file => file)
)
)
)
);
Kód používá řadu zápisů, které je asi potřeba vysvětlit.
Zaprvé zápis e => funkce(e)
je ES6 obdoba function(e) { return funkce(e); }
. Zadruhé zápis funkce().then().catch()
znamená, že funkce vrací Promise
a my reagujeme na úspěch (resolve
) nebo neúspěch (reject
). Všimněte si, že při používání then
/catch
je potřeba uzavírat volání metod pomocí kulatých závorek podobně, jako byste uzavírali bloky (callback) složenými závorkami {}
.
Volání e.resolveWith()
je specifické pro událost fetch
a říká, že náš kód hodlá na request odpovědět, ale potřebuje k tomu nějaký čas. Prohlížeč pak automaticky pozastaví request do doby, než funkce uvnitř responseWith()
vrátí odpověď (podporuje i Promise
a pak čeká na Resolve
nebo Reject
).
Jak je uvedeno výše, pouze jedna služba může sledovat requesty z určitého scope
, ale v rámci jedné služby můžete mít zaregistrováno více funkcí sledujících událost fetch
. V tomto případě platí, že první funkce, která zavolá e.resolveWith()
vyhrává a bude se čekat na její odpověď. Ostatní funkce se při zavolání e.resolveWith()
automaticky ukončí (tím, že vyhodí výjimku). Jedna funkce tak může řešit například načítání HTML stránek a offline režim, druhá lazy-loading obrázků, atd.
Funkce fetch()
je vestavěná funkce pro volání AJAX requestu, která vrací Promise
(obdoba $.ajax()
). My pak následně reagujeme na Resolve
(.then()
), kdy vrátíme odpověď ze serveru a zároveň ji uložíme do cache, a také Reject
(.catch()
), což značí offline režim a v tom případě vrátíme soubor z cache.
Cache funguje tak, že nad proměnnou caches
(což je automaticky vytvořená globální instance třídy Cache
) zavoláme metodu open()
se jménem cache, kterou chceme otevřít (nebo vytvořit, pokud ještě neexistuje). Metoda vrací Promise
a v její Resolve
získáme danou cache, do které můžeme soubor vložit (cache.put(response)
) nebo ho přečíst (cache.match(request)
). Metody cache pracují přímo s requesty události fetch
a odpovědí funkce fetch()
, takže není potřeba je nijak překládat. Metoda cache.match()
znovu vrací Promise
a proto je potřeba z její .then()
vrátit nalezený soubor.
Metoda cache.match()
má jednu (trochu nelogickou) specifičnost, takže vždy uspěje (tedy volá .then()
), ale pokud soubor v cache nenajde, vrátí undefined
. Nicméně metoda .respondWith()
přeloží hodnotu undefined
správně na 404 Not Found
, takže není potřeba to nijak ošetřovat.
Toto je ale pouze základní kód, který můžete dále rozšířit podle potřeby.
Načtení souborů pro offline režim
Pokud má vaše služba poskytovat soubory v offline režimu, může je do cache ukládat v okamžiku jejich načtení (viz výše). Tímto způsobem ale získá pouze soubory, které již byly staženy.
Pokud chcete v offline režimu nabízet i soubory, které nemusí uživatel nutně stáhnout, nebo naopak chcete v offline režimu zobrazit jinou stránku, můžete cache říct, které soubory musí v online režimu stáhnout, aby byly k dispozici:
self.addEventListener('install', service => {
service.waitUntil(caches.open('offline')
.then(cache => cache.addAll([
'offline.html',
'offline.jpg',
'offline.css'
])
)
});
Tento kód služby nejprve počká, než se služba nainstaluje, a pak teprve pokračuje. Tím si zajistí, že bude mít k dispozici přístup do cache. Zároveň, jelikož službu je možno instalovat pouze v online režimu, si zajistí, že bude moci soubory stáhnout z internetu.
Voláním metody service.waitUntil()
zajistíme, že prohlížeč udrží službu v běhu, dokud neprovedeme potřebné akce (zde načtení souborů do cache). V opačném případě by totiž prohlížeč službu, okamžitě po skončení install
, uspal a ona by nemohla reagovat na asynchronní otevření cache. Služba může sebe sama uspat zavoláním metody self.skipWaiting()
.
Zavoláním metody waitUntil()
uvnitř události install
také pozdržíte nastavení stavu installed
a vyvolání události activate
do doby, kdy se provede vše potřebné (zde tedy stažení souborů do cache).
Zavoláním caches.open()
otevřeme nebo vytvoříme cache a pomocí její metody cache.addAll()
jí předáme jména souborů, které chceme stáhnout a uložit. Samotné načtení si již cache řídí sama a tak není potřeba nic dalšího programovat. Jen je potřeba myslet na to, že pro offline režim kromě HTML stránky můžeme potřebovat i další soubory jako jsou obrázky, styly a skripty.
Pokud máme trochu složitější stránku a seznam souborů pro offline režim mám poskytuje server přes nějaké API, můžeme si ho stáhnout a pak předat cache:
self.addEventListener('install', service => {
let request = new Request('/api/list/offline');
service.waitUntil(fetch(request)
.then(response => response.json())
.then(json => caches.open('offline'))
.then(cache => cache.addAll(json.files))
);
});
Služba v příkladu si při instalaci vyžádá vytvoření requestu na adresu API pro vypsání offline souborů (adresa záleží na vašem serveru). Následně požádá prohlížeč o jeho stažení (fetch(request)
) a zároveň o počkání na odpověď (service.waitUntil()
). Po získání odpovědi a převedení na JSON (response.json()
, které je též asynchronní,) přečte pole files
, které předá do cache pro jejich následné stažení.
Poznámka: zavolání service.waitUntil(something)
je prakticky stejné jako použití await something
, jen je to v kontextu služby čitelnější a jasnější, že čekání provádí služba a ne naše funkce. Pokud .waitUntil()
nepoužijete, může prohlížeč ukončit běžící spojení stejně, jako to udělá třeba s AJAX requesty po zavření okna stránky.
V offline režimu pak počkáme, až si prohlížeč požádá o nějaký HTML soubor (tedy stahuje celou stránku) a pokud to selže (tedy je offline), vrátíme mu naši offline stránku:
var offline = false;
self.addEventListener('fetch', e => {
let r = e.request;
//při stažení HTML stránky:
if ('navigate' === r.mode) {
//pokud nejde stáhnout z internetu
e.respondWith(fetch(r).catch(f => {
offline = true;
//získej offline stránku z cache
caches.open('offline')
.then(cache => cache.match('offline.html'))
.then(file => file)
)
}
else if (offline || !navigator.onLine) {
e.respondWith(caches.open('offline')
.then(cache => cache.match(e.request))
.then(file => file)
)
}
});
Služba sleduje stahování HTML stránek a pokud některá selže, vrátí místo ní stránku offline.html
. Zároveň se přepne do offline režimu, takže všechny další soubory (obrázky, styly, atd.) bude také vracet z cache.
Tento přístup bude ale fungovat jen v případě, že vaše stránky posílají plnohodnotné HTML dotazy. Pokud web částečně nebo plně stahuje části stránky přes AJAX, bude potřeba použít jiný přístup.
Aby služba poznala, kdy se uživatel připojí k internetu a už není potřeba vracet soubory u cache, může reagovat na událost online
:
window.addEventListener('online', e => { offline = false });
Registrace PUSH notifikací
Po té, co ověříme, že jsou notifikace podporované (viz výše), můžete si zažádat o povolení.
Poznámka: Doporučuji uložit registraci notifikací do samostatného souboru, který načtete, jen když jsou Service Worker a Push Manager podporovány. Díky tomu budete moci používat ES6 pro jejich registraci. Pamatujte ale na to, že tento soubor se nebude chovat jako služba a tudíž bude mít přístup do globálního scope. Proto byste měli funkce a proměnné vytvářet v uzávěře. Alternativně můžete soubor načíst jako Modul.
Ověření starší registrace
Nejprve se Push Manageru zeptáme, zda již neexistuje registrace z dřívějška:
navigator.serviceWorker.ready
.then(reg => reg.pushManager.getSubscription())
.then(subscription => {
if (subscription) {
return updateSubscription(subscription);
} else {
return createSubscription(reg)
}
)
.catch(f => disableSubscription())
}
Pokud funkce getSubscription()
vrátí objekt, znamená to, že notifikace již byly zaregistrovány a není to tedy potřeba dělat znovu (nicméně může být potřeba je aktualizovat, viz dále).
Pokud getSubscription()
vrátí NULL
, znamená to, že registrace neexistuje a je potřeba o ni požádat. Pokud funkce getSubscription()
selže, znamená to, že o notifikace požádat nemůžeme.
Vytvoření nové registrace notifikací
Nyní se podíváme na funkci createSubscription()
, která požádá o její povolení:
const createSubscription = reg => {
reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: getVapid()
})
.then(details => addSubscription(details))
.catch(f => disableSubscription());
}
Funkce createSubscription()
řekne Push Manageru, aby požádal uživatele o povolení zasílat oznámení. Pokud to uživatel schválí, získáme v .then()
informace o tom, jak push notifikace posílat – což si budeme muset odeslat na server a uložit do databáze (viz dále).
Pokud uživatel notifikace nepovolí nebo dojde k něčemu jinému (např. nejdou zapnout kvůli zabezpečení prohlížeče), vyvolá se Reject (.catch()
) a my na tu musíme příslušně zareagovat.
Parametr userVisibleOnly
, který posíláme do subscribe()
určuje, zda žádáme uživatele (hodnota true
) o to, aby povolil zobrazovaní notifikací (např. informace o nových zprávách, o slevách, apod.). V současné době je toto jediná možnost, jak o notifikace požádat. Teoreticky by uvedením false
bylo možnost požádat pouze o registraci notifikací, které nebudou nic zobrazovat a pouze budou vyvolávat události služby. Tím by například mohl server odeslat službě zprávu o tom, že by měla do cache uložit nový obsah. V tomto případě by uživatel nemusel nic povolovat a prohlížeč by jen rozhodl, zda bezpečnostní nastavení povolují notifikace na pozadí. To ale zatím není možné a i když technicky to tak dělat lze, je vždy potřeba uživatele požádat o to, aby notifikace povolil!
Druhý parametr je vyžadovaný, pokud budete chtít notifikace posílat přímo z vašeho serveru do zařízení uživatele. Pokud budete pouze zobrazovat notifikace ze služby v reakci např. na AJAX volání, server key není potřeba. Jako parametr musíte uvést veřejný klíč, který je potřeba k tomu, aby vaši registraci nemohl někdo zneužít a posílat uživateli nechtěné zprávy. Klíč si můžete vygenerovat pomocí NPM balíku web-push
. Nejprve si nainstalujte NPM (pokud dosud nemáte) a následně v konzoly spusťte:
> npm install -g web-push
> web-push generate-vapid-keys
Druhý příkaz vypíše veřejný a soukromý klíč, kde veřejný klíč můžete přímo uložit do JS souboru pro registraci nebo si ho stahovat přes AJAX, zatímco soukromý klíč musíte uložit do souboru přístupného pouze na serveru (např. config.php
) a používat ho pouze pro odesílání PUSH zpráv.
//jednoduchá implementace
const getVapid = () => urlB64ToUint8Array('váš veřejný klíč');
//Stažení přes API
const getVapid = () {
let vapid = await fetch(new Request('/api/get/vapid'));
return urlBase64ToUint8Array(vapid.json().key);
} //funkce je záměrně synchronní, aby neměnila způsob volání
Funkce urlBase64ToUint8Array()
není v prohlížečích podporována, protože pouze obchází chyby v implementaci použití Base64 klíče v Chrome (webkit) a v budoucnu by se tedy neměla používat. Pro teď si ji musíte stáhnout z GitHubu Mozilly.
V objektu vráceném funkcí subscribe()
získáme informace o registraci notifikací. Vlastnost endpoint
obsahuje URL adresu serveru, na který musíme posílat data, aby se notifikace dostala k uživateli. Ta se liší podle toho, ve kterém prohlížeči se uživatel registroval. V Chrome to tedy bude server Google, ve Firefoxu server Mozzila, atd. Také obsahuje unikátní klíč zařízení uživatele, aby bylo jasné, kam se má upozornění poslat. Je tedy potřeba ji uložit celou přesně tak, jak ji prohlížeč vrátí!
Vlastnost keys
pak obsahuje další hodnoty, které může prohlížeč vyžadovat k tomu, abyste mohli poslat notifikaci na jeho server. Může jít o hodnoty auth
, authToken
nebo p256dh
(ověření vygenerované na základě vaše klíče a klíče prohlížeče), contentEncoding
(určuje jak se mají push zprávy ukládat na server, aby jim prohlížeč rozuměl). Na serveru není potřeba rozumět tomu, co je ve vlastnosti keys uloženo. Stačí je v JSON uložit do DB a před odesláním zprávy převést na objekt a odeslat do knihovny.
Registrace také může obsahovat klíč expiration
, což je datum, do kterého je potřeba o registraci pořádat znovu a tím jí prodloužit. Zatím ji ale implementuje jen Edge, který ale v další verzi přejde na Chromium a ten ji nepoužívá.
Při ukládání endpoint
a keys
do databáze myslete na to, že jeden uživatel si můžete notifikace povolit na více zařízeních, takže vztah uživatel – notifikace by měl být 1:n
. Při odesílání pak myslete na to, že pro každého uživatele může být potřeba odeslat několik zpráv na různé URL adresy.
Odeslání PUSH zprávy ze serveru
Odeslání zprávy není problematika Service Workeru, takže ji zmíním jen okrajově. Pokud používáte jako server Node.js, můžete použít výše uvedený balík web-push. Jen ho nainstalujte do projektu:
> npm install web-push --save
Pro PHP můžete použít knihovnu web-push-php:
> composer require minishlink/web-push
Pro C#, Python a Java najdete soubory na GitHubu push služby (https://github.com/web-push-libs). Ostatní servery mohou využít Node.js a volat funkci přes příkaz (více viz GitHub balíku web-push):
> web-push send-notification \
--endpoint=<url> \
--payload=<text zprávy>
Odeslání push zprávy je obdobné ve všech jazycích (knihovnách):
- Načtete knihovnu
web-push
- Načtete hodnoty
endpoint
akeys
z databáze - Připravíte
payload
, což je text zprávy (buď text nebo JSON) - Připravíte si další nastavení (veřejný a privátní klíč,
TTL
jak dlouho je zpráva platná,urgency
neboli důležitost zprávy, jestli se má poslat přes PROXY, atd.) - Vše odešlete přes metodu
sendNotification()
Hodnota TTL
je počet sekund, jak dlouho může být zpráva uložena na serveru, pokud uživatel zrovna není online. Hodnota 0 znamená, že se zpráva ihned zahodí, pokud ji nelze doručit (např. pokud oznamuje živý přenos, který již později nebude k dispozici). Doporučená maximální hodnota je pod 2 miliony, což odpovídá 4 týdnům (resp. 1 měsíci).
Hodnota urgency
určuje důležitost zprávy a říká, za jakých podmínek může zařízení zprávu stáhnout a zobrazit, aby neplýtvalo baterií. Parametr má vliv pouze u mobilních zařízení a na desktop počítači s LAN se vždy zobrazí všechny upozornění (ale jelikož nevíte, na jaké zařízení zprávu posíláte, je potřeba ji uvést vždy).
high
znamená kritické upozornění, které je potřeba zobrazit vždy i za cenu vybití baterie (např. příchozí hovor, video-chat nebo živý přenos, oznámení o přírodní katastrofě, důležitá schůzka v kalendáři apod.).normal
je zpráva, která se musí stáhnout přes mobilní připojení, ale není potřeba ji zobrazit, pokud má zařízení nízký stav baterie (např. zpráva z chatu, připomínka, časově omezená akce v obchodě, apod.).low
označuje zprávu, která není důležitá a může se zobrazit jen pokud je zařízení připojeno přes WiFi (a stáhne se rychle) a nebo je připojeno k napájení (a nehrozí tedy vybití baterie). Tento typ by měli používat weby z kategorie hry, zpravodajské servery, sociální sítě, apod. které posílají aktualizace obsahu.very-low
zprávy se stáhnout pouze pokud je zařízení připojeno k WiFi a zároveň je připojeno k nabíječce. Tato úroveň by se měla používat pro reklamy a jiné propagační upozornění, u kterých nehrozí, že po pár hodinách či dnech ztratí platnost.
Samozřejmě záleží na vás, zda budete slevové kupóny posílat jako very-low
nebo high
, ale sami zvažte, zda stojí za riziko, že uživatelé notifikace odhlásí jen proto, že jim vaše reklamy vybíjejí baterii mobilu a ruší je třeba v kině.
Velikost zprávy je omezena. V současné době je maximum 4078 znaků/bytů (pro celou zprávu vč. veřejného klíče a dalších dat), ale z bezpečnostních důvodů může být zkrácena na 3052B; starší zařízení mohou mít omezení na 1kB, 512B nebo 240B. Proto je potřeba dávat pozor, co zprávou posíláte (viz dále).
Přijetí Push zprávy
Poté, co odešlete zprávu ze serveru, ji musí služba přijmou. To udělá tak, že si zaregistrujete událost push:
self.addEventListener('push', e => {
//pokud server odesílá čistý text:
let message = await e.data.text();
/pokud server odesílá JSON data:
let data = await e.data.json();
e.waitUntil(self.registration.showNotification(
data.title, {
body: data.text,
icon: data.icon,
badge: data.badge_image
}
);
});
Všimněte si, že data získáte buď metodou text()
nebo json()
podobně jako u fetch()
requestu. Pozor ale na to, že u Push jsou metody synchronní a vrací rovnou string nebo objekt (zatímto u Response
vrací Promise
)!
Důležité pro notifikaci je title
(což může být titulek článku nebo jméno vašeho webu) a body
, které obsahuje samotný text zprávy. Zobrazení icon
a badge
(a případně ještě image
) závisí na prohlížeči a operačním systému – některé systémy zobrazí pouze ikonu a text (Windows 10), jiné naopak mohou zobrazit jen badge v liště (Android), apod.
Ve vlastnosti icon
můžete poslat barevnou ikonu nebo malý obrázek (32×32 až ~256×256), který se zobrazí vedle textu upozornění. Měl by to být tedy obrázek, který se týká přímo textu (např. fotka uživatele u zprávy). Ikona by měla být čtvercová a notifikace ji zobrazí celou, jen ji může zmenšit nebo zvětšit podle potřeby. Ve vlastnosti image
můžete poslat větší obrázek (třeba 1920×1080) a měl by se zobrazit pod textem. Nicméně podle velikosti obrazovky ho může notifikace různě oříznout, takže by to neměl být obrázek, který po oříznutí ztratí smysl (např. obrázek s textem). Vlastnost badge
je pak určena pro malý dvoubarevný obrázek, který se zobrazí v liště (převážně mobilních zařízení) před tím, než uživatel rozbalí jejich seznam. V badge
by tedy měla být ikona, která se týká vašeho webu (typicky favicon
). U badge
ikony počítejte s tím, že bude hodně malá a s málo barvami (obvykle jen černá na průhledném pozadí) a barva se může změnit (např. v nočním režimu se může barva změnit na bílou).
Pro zobrazení notifikace je potřeba zavolat self.registration.showNotification()
, protože jinou zprávu (např. alert()
, Notification
, apod.) není ze služby možno vyvolat. Volání je ale stejné jako new Notification()
z webu.
Také je potřeba zavolat waitUntil()
, protože notifikace se nemusí zobrazit hned (např. musí počkat, než se stáhnou použité ikony). Také (na některých systémech) může notifikace čekat, než se zavře předchozí notifikace, pokud může zobrazit jen jednu.
Je vidět, že konečný vzhled a obsah zprávy určuje až služba v zařízení, takže server může klidně poslat jen ID zprávy (aby nepřekročil omezení délky) a obsah si může stáhnout až zařízení (během přijetí musí být zařízení online, takže stažení dalších dat je možné):
self.addEventListener('push', e => {
let id = await e.data.text();
let request = new Request('/api/get/push/'+id);
let data = await fetch(request);
data = await data.json();
e.waitUntil(self.registration.showNotification(
data.title, {
body: data.text,
icon: data.icon,
badge: data.badge_image,
data: {id}
}
);
});
V závislosti na typu zařízení a verzi prohlížeče může notifikace podporovat i další vlastnosti. Níže uvedené tak mohou a nemusejí fungovat.
Například do parametru vibrate
můžete poslat délky (v milisekundách) jak dlouho má zařízení vibrovat a dělat mezery (vibrate: 500
bude vibrovat půl sekundy; vibrate: [100, 500, 100]
udělá dva krátké záchvěvy s půl sekundovou pauzou mezi nimi). Každé zařízení má maximální dobu, po kterou může vibrovat, takže příliš dlouhé sekvence může oříznout! Pokud vibrate
neuvedete, bude se vibrace řídit výchozím nastavením.
Parametr requireInteraction
určí, že upozornění nelze automaticky zavřít a vždy je potřeba, aby uživatel klikl na nějakou akci nebo křížkem jej zavřel. Parametr sticky
pak úplně zakáže zavření křížkem a vynutí použití kliku nebo akce.
Parametr lang
určuje jazyk zprávy (např. pokud je to potřeba pro správné provedení následné akce) a dir
může přepnout na arabský text (zprava doleva) nebo naopak (rtl
a ltr
).
Parametry timestamp
a renotify: true
(a případně tag
) určují, že znovu zobrazujete starší upozornění (např. pokud uživatel klikl na Odložit). Pomocí silent: true
můžete vypnout zvuk i vibrace; renotify: false
pak vypíná zvuk a vibrace v případě, že je již zobrazeno starší upozornění (z vaší stránky). Parametr noscreen: true
určuje, že notifikace není důležitá a nemá při přijetí zapínat obrazovku.
Do parametru data
můžete poslat libovolné informace, které budete potřebovat pro ošetření události klik nebo akce.
Reakce na kliknutí
Pokud zobrazená zpráva vyžaduje akci (např. návštěvu stránky, odpověď, apod.), může služba reagovat na událost notificationclick
:
const web_url = 'https://my.server.com/push/';
self.addEventListener('notificationclick', e => {
e.notification.close();
let url = web_url + e.notification.data.id;
e.waitUntil(clients.openWindow(url));
});
Funkce musí nejprve zprávu zavřít, aby nepřekážela v oznamovací oblasti a následně musí otevřít nové okno prohlížeče s požadovanou stránkou.
Veškeré parametry, které jste poslali do showNotification()
můžete následně přečíst přes event.notification.*
, takže data
, tag
a další parametry můžete použít k tomu, abyste správně zareagovali.
Více akcí
Pokud vám jednoduché kliknutí na upozornění nestačí, můžete do notifikace přidat tlačítka (interně zvané actions). Například pokud zobrazujete upozornění na chat zprávu, můžete přidat Odpovědět a Ignorovat, pokud jde o upozornění na úkol, mohou být akce Dokončit a Odložit. Akce je doporučeno kombinovat s requireInteraction: true
, aby se zpráva nezavřela dřív, než uživatel nějakou akci zvolí.
Tlačítka se přidávají do druhého parametru ve vlastnosti actions
. Definují se jako pole objektů s vlastnostmi action
(identifikátor), title
(přesněji label, neboli to, co se zobrazí uživateli) a volitelně icon
(URL obrázku). Kolik akcí můžete přidat se dozvíte z vlastnosti Notification.maxActions
. Podle toho, kolik akcí přidáte se můžete také lišit vzhled upozornění.
self.addEventListener('push', e => {
let data = e.data.json();
//definuje základní tlačítka pro zprávu
//první akce slouží pro odpověď a smazání
let actions = [
{ action: 'reply',
title: 'Zobrazit' },
{ action: 'ignore',
title: 'Ignorovat' }
];
//pokud můžeme mít třetí akci, přidáme smazání
if (2 < Notification.maxActions) {
actions.push({ action: 'delete',
title: 'Smazat',
icon: 'img/trash.png'
});
//a změníme název první akce
actions[0].title = 'Odpovědět';
}
e.waitUntil(self.registration.showNotification(
'Zpráva od ' + data.sender, {
body: data.message,
data: data,
requireInteraction: true,
actions: actions
}
);
});
Zjištění, na které tlačítko uživatel klikl, je stejné, jako ošetření jednoduchého kliknutí. Jen si stačí přečíst vlastnost action
:
const web_url = 'https://my.server.com/';
self.addEventListener('notificationclick', e => {
let id = e.notification.data.id;
if ('reply' === e.action) {
e.notification.close();
let url = web_url + 'chat/reply/' + id;
e.waitUntil(clients.openWindow(url));
}
else if ('delete' === e.action) {
e.waitUntil(fetch(web_url
+ 'api/delete/' + id)
.then(r => e.notification.close())
);
}
else e.waitUntil(fetch(web_url
+ 'api/mark-read/' + id)
.then(r => e.notification.close())
);
});
Pokud uživatel klikl na Odpovědět (resp. Zobrazit), zavřeme upozornění a v prohlížeči otevřeme okno s příslušnou zprávou.
Pokud uživatel klikl na Ignorovat nebo Smazat, pouze přes API označíme zprávu jako přečtenou nebo smazanou. Notifikaci ale zavřeme až v okamžiku, kdy server odpoví, že akci provedl, aby měl uživatel kontrolu, že se opravdu zpráva smazala (a např. mohl to provést znovu v případě, že zrovna přišel o připojení).
Poznámka: Všimněte si, že akci Ignorovat provádíme ve větvi else
(tedy bez další podmínky). To je proto, že pokud uživatel klikne na upozornění jinde než na tlačítkách, stále dojde k vyvolání událostinotificationclick
(které bude mít hodnotu action
prázdnou) a my ho můžeme použít stejně jako klik na Ignorovat. Navíc některé systémy nemusejí akce podporovat, takže byste vždy měli reagovat na obyčejné kliknutí bez action
.
Detekce zavření upozornění
Ať už máte v události akce nebo jen reagujete na klik, může stále uživatel upozornění zavřít bez kliknutí (jelikož možnost sticky
není zatím plně podporována). Pokud potřebujete na zavření reagovat (např. pro statistické účely), můžete sledovat událost notificationclose
:
self.addEventListener('notificationclose', e => {
e.waitUntil(fetch('/api/mark-read'
+ e.notification.data.id));
});
Tato funkce po zavření upozornění označí zprávu jako přečtenou, čímž dá serveru najevo, že uživatel na upozornění zareagoval a server si může do statistik uložit, že je uživatel aktivní.
Důležité je si uvědomit, že pokud dojde k zavření události v důsledku nějaké globální nebo systémové události (např. uživatel vymaže všechna upozornění nebo se systém restartuje), k vyvolání notificationclose
nedojde. Událost tedy označuje situaci, kdy uživatel interagoval přímo s vaším upozornění!
Reakce na push notifikaci v offline režimu
Dejte si pozor na to, že i když se notifikace stahuje a zobrazuje v online režimu, reakce na ni (kliknutí nebo zavření) může přijít až po nějaké době, kdy už je zařízení offline (například: uživatel sedí v metru a prohlíží dříve přijatá upozornění). Pokud tedy chcete odeslat data na server, je potřeba reagovat na fetch().catch()
a případě podat sync
požadavek.
V případě, že po kliknutí chcete otevřít stránku v prohlížeči, může být potřeba otevřít nějakou offline stránku, která uživatele upozorní na to, že je v offline režimu, a nabídne mu automatické (reakce na online
událost) nebo manuální (odkaz) přejití do online stránky.
Zajímavé řešení by bylo zobrazit offline stránku, ze které si požádáte o sync
a z něj pak zobrazíte dané upozornění znovu. Ale zatím nemám ověřeno, že je to technicky možné.
Push notifikace jako informace o aktualizaci
Push notifikace nemusíte používat pouze k zobrazování informací uživateli, ale můžete je použít k interní komunikaci mezi serverem a zařízením.
Pokud server dostane nějaké nové zprávy (např. nové články), může zařízení poslat zprávu a ta, místo aby otravovala uživatele upozorněním, může data stáhnout na pozadí a zobrazit je až v okamžiku, kdy uživatel otevře vaši stránku – a to i v případě, že bude zrovna offline.
self.addEventListener('push', e => {
let data = e.data.json();
if (data.isUpdate) {
e.waitUntil(caches.open('offline')
.then(cache => {
cache.put('/articles.json,
data.articles);
for (id of data.articles) {
cache.add('/articles/'+id);
}
})
);
}
else {
//ošetření ostatních notifikací
}
});
Tento kód kontroluje, zda notifikace (zaslaná jako JSON) obsahuje vlastnost isUpdate
, a pokud ano, tak očekává, že vlastnost articles
bude obsahovat pole názvů nebo IDček článků (tak, jak jsou potřeba pro URL) a všechny je přidá do cache (metoda add()
provede fetch()
a následně cache.put()
).
Stránka offline.html
pak může fungovat tak, že si přes AJAX stáhne soubor articles.json
, který je uložen v cache a tedy dostupný i offline, a z něj pak získá seznam článků, které má uložené a může je tedy zobrazit.
Poznámka: alternativa k articles.json
by byla ukládat do cache články pomocí GET parametrů (např. /articles/?id=1
) a následně použít metodu cache.keys('/articles/', { ignoreSearch: true})
, která pak vrátí všechny requesty z cache, které mají adresu /articles/
nezávisle na parametrech za otazníkem. Tento způsob by byl lepší v případě, že chcete přes PUSH update odesílat čísla jen nových článků a pak zobrazit staré i nové.
Synchronizace na pozadí
Synchronizací na pozadí můžete pokrýt několik různých situací:
- Offline stránka odešle nějaká data (např. odpověď na zprávu, nové fotky, apod.), ale musí počkat, až bude uživatel online.
- Uživatel prohlíží online stránku, přejde do offline (např. ve vlaku jdete do tunelu) a následně odešle data (např. odpověď na zprávu, nové fotky, apod.). V normálním případě by o taková data pravděpodobně přišel.
- Služba detekuje, že uživatel přešel do online režimu a stáhne si ze serveru aktualizace.
Hlídání Online a Offline režimu
Služba i ostatní stránky mohou sledovat přepínání online a offline režimu pomocí událostí online
a offline
. Případně ve funkci se dá stav ověřit vlastností navigator.onLine
(takže není potřeba vytvářet online
a offline
handlery jen proto, abyste si stav uložili).
Funkce reagující na fetch
tak nemusí čekat, jestli metoda fetch()
selže, a v případě offline režimu může rovnou vrátit offline stránku z cache.
self.addEventListener('offline', e
=> console.log('Ztracen přístup k internetu'));
self.addEventListener('online', e
=> console.log('Přístup k internetu obnoven'));
self.addEventListener('fetch', r => {
if (navigator.onLine) {
e.respondWith(fetch(r));
}
else {
e.respondWith(getOfflinePage());
}
});
Další příklady použití výše uvedených událostí jsou:
- při detekci offline režimu můžete upozornit uživatele na to, že nebude dostávat aktualizace a připomenou mu, že stále může data odesílat (viz dále)
- po detekci online režimu z
offline.html
můžete nabídnout přechod na online verzi stránek (nedělal bych to ale automaticky, protože uživatel může mít rozdělanou práci).
I když můžete online a offline režim detekovat, je potřeba pamatovat na to, že detekce změny může mít nějaké zpoždění (ať už proto, že změnu režimu hned nepozná, nebo proto, že má hodně dalších služeb, které musí také upozornit) a že k přechodu do offline režimu může dojít až v okamžiku, kdy čekáte na odpověď, a v tom případě požadavek selže. Detekce tedy není samo-spásná a je stále potřeba myslet na všechny situace a správně reagovat na .catch()
u Promise
.
Odeslání dat v offline režimu
Upozornění: Tato funkce je sice podporována ve všech nejnovějších verzích prohlížečů, které podporují Service workery, ale syntaxe ani funkčnost není sjednocena, takže je možné, že se jednotlivé implementace liší a že bude v budoucnu funkce přepracována! Není tedy dobré se na ni 100% spoléhat.
Pokud chcete z vaší offline stránky odeslat data na server, nemůžete to samozřejmě udělat přes klasický AJAX nebo POST formuláře, protože by selhal. Stačí si ale zaregistrovat požadavek v Sync Manageru a ten se následně postará o to, aby se data odeslala v online režimu.
if (navigator.onLine) {
//jsme online, můžete odeslat hned
$.ajax(... submit data);
}
else { //offline, musíme počkat
navigator.serviceWorker.ready.then(
function(service) {
service.sync.register('submitData');
}
);
}
Pomocí service.sync.register
si zaregistrujeme synchronizační požadavek pod jménem submitData
. Sync Manager následně čeká na online režim a pak vyvolá událost sync
ve službě (pro každý zaregistrovaný požadavek jednu). Pokud vytvoříte sync požadavek, když je prohlížeč online, vyvolá se sync událost okamžitě. Službě pak stačí přečíst si jméno požadavku z vlastnosti tag
a provést potřebnou akci:
self.addEventListener('sync', e =>
if ('submitData' === e.tag) {
e.waitUntil(submitData());
}
);
Uvnitř události sync
má metoda e.waitUntil()
ještě jednu vlastnost a to, že pokud vrácený Promise
(zde z metody submitData()
) selže (reject()
), Sync Manager to pozná a vyvolá další sync
událost o něco později (třeba po té, co se dokončí ostatní sync
a fetch
události, nebo když detekuje lepší připojení jako je WiFi). Prostě to bude zkoušet, dokud daný sync
požadavek neskončí úspěchem.
Pokud vás nyní zajímá, co by měla obsahovat metoda submitData()
a jak jí můžete odeslat data ze stránky, tak je to možné buď přes message (viz dále), nebo s využitím ostatních prostředků Javaskriptu, jako je Cache
, LocalStorage
(ten ale není pro Service Workery vhodný!) nebo IndexedDB
.
Přes message můžete poslat službě libovolná data. Jak s nimi naloží, je na ní. Zde je příklad odeslání dat v offline režimu:
//offline.html
if (navigator.onLine) {
//jsme online, můžete odeslat hned
$.ajax('/submit', {data: getData() });
//příklad pro jQuery,
//můžete ale použít i Fetch stejně jako níže
}
else { //offline, musíme počkat
navigator.serviceWorker.ready
.then(function(service) {
//pošli službě data k odeslání
navigator.serviceWorker.controller
.postMessage({submitData: getData()});
//zaregistruj sync požadavek
service.sync.register('submitData');
});
}
//service.js
let submitData = null; //pro uložení dat
self.addEventListener('message', e => {
if ('submitData' in e.data) {
//uložení přijatých dat pro pozdější použití
submitData = e.data.submitData;
}
});
self.addEventListener('sync', e =>
if ('submitData' === e.tag) {
e.waitUntil(submitData());
}
);
const submitData() {
return fetch('/submit', {
//odeslání dat přes AJAX v JSON formátu
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify(submitData)
})
.then(response => {
submitData = null; //vymazání uloženích data
return response;
};
}
Alternativně můžete zneužít tag a přímo v něm poslat JSON objekt:
//stránka:
service.sync.register(JSON.stringify({
apiMethod: 'save',
params: { body: data }
});
//služba:
self.addEventListener('sync', e => {
let data = JSON.parse(e.tag);
if (data.hasOwnProperty('apiMethod')) {
submitData(data.method, data.params);
}
});
Sync tag musí být unikátní
Pokud pošlete stejný sync požadavek zatímco se čaká na online režim (nebo předchozí sync selhal), prohlížeč ho tiše zahodí.
Pokud potřebujete skutečně poslat jeden požadavek vícekrát, musíte zajistit, aby byl unikátní. V případě, že tag je jméno události, o kterou žádáte, musíte k ní přidat unikátní identifikátor, který pak budete v sync handleru ignorovat:
//stránka:
service.sync.register('submitData'
+ '|' + (new Date()) + Math.random());
//služba:
self.addEventListener('sync', e => {
let tag = e.tag.split('|').shift();
if ('submitData' === tag) {
submitData();
}
});
Pokud v tagu posíláte JSON objekt, postačí k jeho unikátnosti do něj zahrnout další unikátní klíč(e):
//stránka:
service.sync.register(JSON.stringify({
apiMethod: 'save',
params: { body: data },
created: new Date(),
uid: Math.random()
});
Použití new Date()
zajistí, že sync požadavky budou v čase unikátní (každou sekundu). Kombinace s Math.random()
pak zajistí, že bude tag unikátní i v případě dvou požadavků během jedné sekundy. Naopak kombinace s new Date()
zajišťuje, že po určité době nedojde k vygenerování stejného náhodného čísla. Šance, že během jedné sekundy vygenerujete dva sync requesty se stejným náhodným číslem, je zanedbatelná.
Propojení stránky a služby
Jak již bylo uvedeno výše, služba a otevřené stránky spolu mohou komunikovat přes postMessage()
. Aby to ale bylo možné, musejí se stránka a služba nějak spojit. Většina obsahu této kapitoly je již zmíněna výše, ale zde je sumarizace.
Jednorázové odeslání zprávy
Stránka se se službou automaticky propojí tím, že že zavolá navigator.serviceWorker.register()
. Uvedením určité URL pak stránka říká, ke které službě se chce připojit. Vícenásobným zavoláním register()
s různými URL a scope
může stránka zaregistrovat více služeb, ale aktivní bude jen ta, která splňuje podmínku na scope dané stránky Odkaz pro posílání zpráv propojené službě získá stránka ve funkci reagující na register().then(reg => {})
.
Pokud chce stránka posílat zprávy službě, musí je posílat z metod definovaných v uzávěře (Closure) této funkce (nebo funkce navigator.serviceWorker.ready.then(reg => {})
). Zprávu lze pak poslat metodou reg.active.postMessage()
. Zprávu lze také poslat přes navigator.serviceWorker.controller.postMessage()
, ale je třeba dát pozor na to, že vlastnost controller
může být NULL
v okamžiku, kdy služba teprve čeká na aktivaci. Na druhou stranu lze vlastnost controller
využít k detekci, zda lze zprávy posílat a pokud ne, vyřešit problém jinak.
//po kliku na tlačítko pošli zprávu službě
el.onclick = function(e) {
if (navigator.serviceWorker.controller) {
navigator.serviceWorker.controller
.postMessage({
action: 'click',
target: e.target.id
});
}
else {
alert('Služba není aktivována!');
}
}
Z druhé strany služba se propojí se stránkami, které ji registrují buď po zavolání metody e.waitUntil(clients.claim())
z události activate
nebo automaticky po jejich znovu načtení (reload). Služba ale může být propojena na více stránek ze svého scope
, pokud jsou současně otevřeny v různých oknech prohlížeče.
Seznam otevřených oken (klientů) získá služba zavoláním clients.matchAll().then(clientList => {})
, případně clients.matchAll({includeUncontrolled: true})
pro získání i oken, které teprve čekají na aktivaci služby.
Proměnná clientList
je pole klientů, které může služba projít pomocí FOR-OF
nebo přes .map()
. Každý klient má vlastnost client.url
, pomocí které může služba určit, kterým klientům chce zprávu poslat. Zpráva se pak posílá metodou client.postMessage()
.
//po přechodu do online aktualizuj data
// a pošli je všem otevřeným offline stránkám
self.addEventListener('online', e =>
e.waitUntil(fetch('/api/update')
.then(r => clients.matchAll())
.then(list => for (client of list) {
if (client.url.match(/^\/offline.html/i)) {
client.postMessage({
action: 'updated',
data: r.json()
});
}
}))
);
Uvnitř událostí vyvolaných konkrétním klientem, jako jsou fetch
a sync
, může služba poslat zprávu přímo danému klientovi, protože událost obsahuje hodnotu clientId
. Zprávu pak lze poslat metodou clients.get(e.clientId).postMessage()
. Například pokud klient požádal o odeslání formuláře přes sync
, může mu služba dát vědět, že již data odeslala a stránka může tuto informaci předat uživateli.
//po uložení dat pomocí sync informuj klienta,
//který vytvořil sync požadavek a poslal data
self.addEventListener('sync', e =>
e.waitUntil(fetch('/api/sync/' + self.syncId)
.then(r => clients.get(e.clientId)
.postMessage({synced: self.syncId})
)
);
Ve zprávě můžete poslat jakoukoliv hodnotu (řetězec, číslo, apod.) nebo objekt, který je tzv. klonovatelný (to znamená, že nejde například poslat objekt window
nebo self
, které jsou unikátní pro konkrétní stránku nebo službu).
Přijetí zprávy
Služba může zprávy přijímat sledováním své události message
:
self.addEventListener('message', e => {
let message = e.data;
let client = clients.get(e.source.id);
});
Pokud chce klient reagovat na zprávy od své služby, musí si také zaregistrovat událost message
, ale aby bylo jasné, že jde o zprávy od služby, musí to udělat na objektu navigator.serviceWorker
:
navigator.serviceWorker.addEventListener('message',
function(e) {
var message = e.data;
var service = navigator.serviceWorker.controller;
}
);
Zpráva s odpovědí
Pokud vám nestačí výše uvedené odeslání jednorázové zprávy, můžete mezi klientem a službou vytvořit kanál, kterým mohou komunikovat. Tím získáte možnost přijetí odpovědi na konkrétní zprávu.
Nejprve na straně klienta vytvořte instanci třídy MessageChannel
. Ta nabízí dva porty (port1
a port2
, které přestavují konce kanálu; chcete-li pipe), na které se mohou navázat klient a služba (tak, že každý bude odchytávat jeden port; tedy konec kanálu).
//odeslání zprávy
var channel = new MessageChannel();
channel.port1.onmessage = function(e) {
process(e.data); //zpracování odpovědi
}
navigator.serviceWorker.controller
.postMessage(zprava, [channel.port2])
//Získání zprávy a odeslání odpovědi
self.addEventListener('message', e => {
let message = e.data;
let responsePort = e.ports[0];
responsePort.postMessage(process(message));
});
Klient vytvoří kanál a sám se pověsí na první konec (port1
). Následně pošle službě zprávu a sdělí jí, že má poslat odpověď do druhého konce kanálu (port2
). Když pak služba odpověď zpracuje a pošle zprávu do svého portu, klient pak na své straně tuhle zprávu získá v callbacku (port1.onmessage
).
Kromě MessageChannel
(která je určena k zasílání textových řetězců), můžete službě poslat jakýkoliv objekt typu transferable (např. ArrayBuffer
, ImageBitmap
nebo OffscreenCanvas
, které většinou slouží k zasílání binárních dat resp. obrázků), do kterých může služba uložit požadovaná data.
Poznámka: MessageChannel
je jen prázdný obal obou portů. Pomocí destrukturalizace objektů můžete rovnou vytvořit dvě samostatné proměnné bez nutnosti ukládat samotný kanál. Díky tomu je také můžete trochu lépe pojmenovat:
const {sender, response} = new MessageChannel();
response.onmessage = function(e) { ... };
navigator.serviceWorker.controller.postMessage(msg, [sender]);
Pokud chcete jít ještě dál a obalit posílání zpráv do Promise
, není to žádný problém:
function postMsg(target, message) {
return new Promise(function(resolve) {
const {sender, response} = new MessageChannel();
response.onmessage = function(e) {
try { resolve(JSON.parse(e.data)); }
catch (e) { resolve(e.data); }
}
if ('object' === typeof message) {
message = JSON.stringify(message);
}
target.postMessage(message, [sender]);
});
}
//Použití:
postMsg(navigator.serviceWorker.controller, 1)
.then(function(response) {
process(response);
})
;
postMsg(navigator.serviceWorker.controller, {id:1})
.then(function(response) {
process(response.id);
})
;
//Zpracování v target
self.onmessage = function(e) {
let message;
try { JSON.parse(e.data); }
catch (e) { message = e.data; }
response = process(message.id);
if ('object' === typeof response) {
response = JSON.stringify(response);
}
e.ports[0].postMessage(response);
};
„Služba sice může sama sebe udržet v běhu po určitou dobu (řádově sekundy nebo minuty), ale nemůže běžet nepřetržitě několik hodin nebo dní…“
Co to pro mě přesně znamená? Pokud potřebuji udržet data pro uživatele k dispozici offline když si web otevře i po třeba 5 hodinách co tam byl naposledy a už je offline, tak když už mezitím služba neběží (browser ji zabil), tak už se znovu nespustí a uživatel se nedostane ani k věcem z cache dokud nebude zase online? Nebo se na to pokud je to v cache naopak spolehnout můžu? Díky
To, zda služba běží nebo ne (je „uspaná“ nebo „zabitá“) nemá vliv na to, zda může reagovat na dedikované události prohlížeče (push, fetch, atd.). Prohlížeč si při instalaci zapamatuje, na jaké události služba reaguje a sám ji probudí před vyvoláním události.
Pokud tedy chcete použít službu pro nabízení offline souborů z cache, je potřeba službu zaregistrovat jako listener pro „fetch“. Díky tomu prohlížeč bude vědět, že kdykoliv chce stáhnout soubor, musí službu probudit a poslat jí fetch požadavek.
Cache je nezávislá na službě a zůstává přístupná, i když je služba uspaná a dokonce i potom, co ji aktualizujete nebo odinstalujete.