Maxim Salnikov
@webmaxru
How to use what service worker already knows
Products from the future
UI Engineer at ForgeRock
Service Worker API
install, activate, fetch, backgroundfetchsuccess, backgroundfetchfail, backgroundfetchclick
sync
push, notificationclick
paymentrequest
Website
Service-worker
Browser/OS
Event-driven worker
'install'
Parsed
Installing
Activating
Redundant
'activate'
Waiting
Active
Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications.
Flagged
OS
My App
Define the set of assets required to show the minimum viable UI
New version is available.
Reload page?
Service worker
install: put the assets into Cache Storage
activate: clear Cache Storage from the previous app version assets
fetch: if the asset is in Cache Storage serve it from there. Otherwise — download and serve it (and cache it)
Build time
Register service worker and listen to its lifecycle events (updates in particular)
Website/webapp
PWA Feature Detector
Web API Confluence
if ('serviceWorker' in navigator) {
// Registering service worker
}
if ('SyncManager' in window) {
// Implement offline-ready network features
}
if (!('PushManager' in window)) {
// Hide UI for Web Push subscription
}
if ('actions' in Notification.prototype) {
// We can use different actions
}
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw-workbox.js')
.then(...);
}
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw-workbox.js')
.then(...);
});
}
platformBrowserDynamic()
.bootstrapModule(AppModule)
.then(() => {
// Регистрация сервис-воркера
});
const appShellFilesToCache = [
...
'./non-existing.html'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('appshell').then((cache) => {
return cache.addAll(appShellFilesToCache)
})
)
})
Chrome | <6% of free space |
Firefox | <10% of free space |
Safari | <50MB |
IE10 | <250MB |
Edge | Dependent on volume size |
if ('storage' in navigator && 'estimate' in navigator.storage) {
navigator.storage.estimate().then(({usage, quota}) => {
console.log(`Using ${usage} out of ${quota} bytes.`);
});
}
const appShellFilesToCache = [
'./styles.css',
...
'./styles.css'
]
event.waitUntil(
caches
.open('appshell').then(cache => {
return cache.addAll(['./bad-res.html'])
.catch(err => {
console.log(err)
})
})
);
event.waitUntil(
caches
.open('appshell').then(cache => {
return cache.addAll(['./bad-res.html'])
.catch(err => {
console.log(err)
throw err
})
})
);
const appShellFilesToCache = [
...
'https://workboxjs.org/offline-ga.min.svg'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open('appshell').then((cache) => {
return cache.addAll(appShellFilesToCache)
})
)
})
fetch(event.request).then( response => {
if (response.ok) {
let copy = response.clone();
caches.open('runtime').then( cache => {
cache.put(request, copy);
});
return response;
}
})
fetch(event.request).then( response => {
if (response.ok || response.status === 0) {
let copy = response.clone();
caches.open('runtime').then( cache => {
cache.put(request, copy);
});
return response;
}
})
navigator.serviceWorker.register('sw-handmade.js')
.then(registration => {
if (registration.waiting) {
// Show "App was updated" prompt to reload
}
})
I'm a PWA and I'm always fresh. But what you see is the outdated version.
SW Boot
Navigation request
SW Boot
Navigation request
addEventListener('activate', event => {
event.waitUntil(async function() {
// Feature-detect
if (self.registration.navigationPreload) {
// Turn it on!
await self.registration.navigationPreload.enable();
}
}());
});
addEventListener('fetch', event => {
event.respondWith(async function() {
// Best scenario: take if from the Cache Storage
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
// OK scenario: use navigation preload response
const response = await event.preloadResponse;
if (response) return response;
// Worst scenario: fetching from the network :(
return fetch(event.request);
}());
});
event.respondWith(async function() {
const response = await fetch(event.request);
const buffer = await response.arrayBuffer();
const WebPDecoder = await fetchWebPDecoder();
const decoder = new WebPDecoder(buffer);
const blob = await decoder.decodeToBMP();
return new Response(blob, { headers: { "content-type": "image/bmp",
"status": 200 } });
}());
npm install -g hint
hint https://airhorner.com
npm install -g lighthouse
lighthouse https://airhorner.com
workbox.core.setCacheNameDetails({precache: 'new-name'});
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', () => {
self.clients.matchAll({type: 'window'}).then(tabs => {
tabs.forEach(tab => {
tab.navigate(tab.url);
});
});
});
Cache-Control: no-cache
navigator.serviceWorker.register(`/sw.js?v=${VERSION}`);
navigator.serviceWorker.register('/sw.js', {
updateViaCache: 'none'
})
const swReg = await navigator.serviceWorker.register("/sw.js");
await swReg.paymentManager.instruments.set(
"My Pay's Method ID",
{
name: "My Pay's Method",
method: "https://my.pay/my-method",
}
);
self.addEventListener("paymentrequest", event => {
// Open the window with the payment method UI
event.openWindow('https://my.pay/checkout')
});
The method (via
The HEAD request to this
In the method's Web App Manifest, there is a
const registration = await navigator.serviceWorker.ready;
await registration.backgroundFetch.fetch(
'my-series',
['s01e01.mpg', 's01e02.mpg'],
{
title: 'Downloading My Series',
downloadTotal: 1000000000
}
);
const bgFetches =
await registration.backgroundFetch.getIds();
console.log(bgFetches);
addEventListener('backgroundfetchsuccess', event => {
event.waitUntil(
(async function() {
try {
// Put the responses to Cache Storage
...
await event.updateUI({ title: `Downloaded!` });
} catch (err) {
await event.updateUI({ title: `Fail: ${err}` });
}
})()
);
});
periodicsync