PWAs 2019 | Service Worker Cookbook (@GitHub)
Make Installable :: Web App Manifest (JSON) | w3.org
<!-- Startup configuration -->
<link rel="manifest" href="manifest.webmanifest">
<!-- Fallback application metadata for legacy browsers -->
<meta name="application-name" content="AppNameShort">
<link rel="icon" sizes="16x16 32x32 48x48" href="lo_def.ico">
<link rel="icon" sizes="512x512" href="hi_def.png">
HTTP Header:
Content-Type: application/manifest+json
-
{ "name": "App Name Full", "short_name": "AppNameShort", "description": "Derp derp ...", "icons": [ { "src": "icons/icon-32.png", "sizes": "32x32", "type": "image/png", "purpose": "maskable" // maskable (20% padding) }, // ... { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png" "purpose": "any" // transparent } ], "start_url": "index.html", "display": "fullscreen", "theme_color": "#ff0066", "background_color": "#ff0066" }
App Shell Model
Segregate application (and its caches);
shell
(static/assets) vs.data
(dynamic/content).const appCaches = [ { name: `"${etag.shell}"`, type: "shell", urls: [ "/", "offline.html", "404.html", "index.html", "scripts/main.js", "styles/base.css", "images/favicon.ico" ] }, { name: `"${etag.data}"`, type: "data", urls: [ "/", "data/file1", "data/file1.1.png", "data/file2", "data/file2.1.png", "data/huge" ] }, ]
Obsolete cache is purged per change of
etag
value(s), per "activate
" event.May add other named/Etagged caches, e.g., to prefetch popular content (
name:
popular
).JSON Cache — segregate the cache declaration (JSON), placing it in a separate file (
.json
) instead of as a variable in the service worker (.js
); cache its references upon service worker install.
Service Worker API
Service worker lifecycle
To make sure service workers don’t break websites, they go through a strictly defined lifecycle. This makes sure that there is only one service worker controlling your website (and therefore only one version of your site exists).
1. Register :: navigator.serviceWorker.register
@
app.js
const sw = 'sw1.js' if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register(sw) .then(onResolved, onRejected) })
Accepts params:
navigator.serviceWorker.register('/sw.js', {scope: './limit-per-here'})
… a service worker can't have a scope broader than its own location, only use the scope option when you need a scope that is narrower than the default.
2. Install :: CacheStorage
Interface
@
sw1.js
self.addEventListener('install', (event) => { event.waitUntil(caches.keys() .then((keyList) => { return Promise.all( appCaches.map((ac) => { // ... caches.open(ac.name) .then((cn) => { // ... cn.addAll(ac.urls) // ... }) }) }) // ...
- Note "
caches
", @caches.keys()
, is a global read-only variable (WindowOrWorkerGlobalScope.caches
); an instance ofCacheStorage
, which isundefined
unless by HTTPS (@ Chrome/Safari). - Service Worker file (
sw1.js
) location sets its scope; place in webroot along with its parent, e.g.,index.html
.
- Note "
If anything goes wrong during this phase, the promise returned from
navigator.serviceWorker.register
(@app.js
) is rejected.
Force Activation upon Install :: self.skipWaiting()
self.addEventListener('install', (event) => {
event.waitUntil(/* ... */)
.then(() => self.skipWaiting())
3. Activate :: waitUntil()
@
sw1.js
self.addEventListener('activate', (event) => { const whiteList = appCaches.map((thisCache) => thisCache.name) event.waitUntil(caches.keys() .then((keysList) => { return Promise.all( keysList.map((key) => { if (whiteList.indexOf(key) === -1) { // ... return caches.delete(key) } }) ) }) }
When you successfully install the new service worker, the activate event will be fired. The service worker is now ready to control your website –– but it won’t control it yet. The service worker will only control your website when you refresh the page after it’s activated. Again, this is to assure that nothing is broken.
The window(s) of a website that a service worker controls are called its
clients
. Inside the event handler for theinstall
event, it’s possible to take control of uncontrolled clients by callingself.clients.claim()
.
self.addEventListener('activate', e => self.clients.claim())
Intercepting Requests :: per Strategy | @ WorkBox
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then((response) => {
// Per STRATEGY: Network First, Cache Falling Back to Network, ...
Save/Cache POST
@ offline
-
.catch(err => { // ... if(method === 'POST') { cacheApiRequest(requestClone) return new Response(JSON.stringify({ message: 'POST request was cached' })) } })
BackgroundSynch
API (Not well supported)
self.addEventListener('sync', (event) => {
if (event.tag === 'syncAttendees') {
event.waitUntil(doSynch())
}
})
… a sync event will be emitted when the system decides to trigger a synchronization. This decision is based on various parameters: connectivity, battery status, power source, etc. ; so we can not be sure when synchronization will be triggered.
Add to Home Screen :: beforeinstallprompt
event (@ Chrome)
window.addEventListener('beforeinstallprompt', (e) => {
// Stash the event so it can be triggered later.
deferredPrompt = e
// Update UI notify the user they can add to home screen
showInstallPromotion()
})
WorkBox
Other
- Web App Manifest
mkcert
:: HTTPS @localhost
- PWA Workshop
- Using Service Workers
- Service Worker Cookbook (code)
- Service Worker API
- Service Workers API
- web.dev