Maxim Salnikov
@webmaxru
What did we learn from
3 years of exploring PWA idea?
Products from the future
UI Engineer at ForgeRock
Progressive web apps use modern web APIs along with traditional progressive enhancement strategy to create cross-platform web applications.
Dev builds
Service Worker
API
Web App Manifest
Web App
Service worker
Browser / WebView
Event-driven worker
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) {
// Consider using action buttons
}
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(() => {
// Service worker registration
});
self.addEventListener('install', () => {
self.skipWaiting();
});
self.addEventListener('activate', () => {
self.clients.matchAll({type: 'window'}).then(tabs => {
tabs.forEach(tab => {
tab.navigate(tab.url);
});
});
});
navigator.serviceWorker.getRegistrations()
.then((registrations) => {
for(let registration of registrations) {
registration.unregister()
}
})
self.addEventListener('activate', event => {
event.waitUntil(self.clients.claim());
self.registration.unregister();
});
Cache-Control: no-cache
navigator.serviceWorker.register(`/sw.js?v=${VERSION}`);
navigator.serviceWorker.register('/sw.js', {
updateViaCache: 'none'
})
const appShellFilesToCache = [
...
'./non-existing.html'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(cacheName).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 = [
...
'https://workboxjs.org/offline-ga.min.svg'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(cacheName).then((cache) => {
return cache.addAll(appShellFilesToCache)
})
)
})
const noCorsRequest =
new Request('https://workboxjs.org/offline-ga.svg', {
mode: 'no-cors'
});
fetch(noCorsRequest)
.then(response => cache.put(noCorsRequest, response));
I have one "real" codebase, the PWA. Then I have 3 "wrapper" projects: Windows UWP app, Java app for Android Studio, Objective C app for XCode. All 3 wrapper projects load up the PWA.
npm install -g sonarwhal
sonarwhal --init
sonarwhal https://airhorner.com
npm install -g lighthouse
lighthouse https://airhorner.com
A newer version of the app is available. Refresh
const updatesChannel = new BroadcastChannel('precache-updates');
updatesChannel.addEventListener('message', event => {
console.log('Cache updated', event.data.payload.updatedUrl);
// Show a prompt "New version is available. Refresh?"
});
workbox.precaching.addPlugins([
new workbox.broadcastUpdate.Plugin('precache-updates')
]);
window.addEventListener("beforeinstallprompt", event => {
// Suppress automatic prompting.
event.preventDefault();
// Show custom install button
installButton.classList.remove('hidden');
// Bind onclick event - on the next slide
});
installButton.addEventListener("click", e => {
event.prompt(); // event - from beforeinstallprompt
event.userChoice
.then( choiceResult => {
console.log(`Choice is: ${choiceResult.outcome}`);
// Hide custom install button
installButton.classList.add('hidden');
})
});
Allow users to unsubscribe.
Otherwise they'll block!
Online check in is available
MyAirline
myairline.com
Flight
DY1043
Depart
21.09 13:45
Click here to check in now
myairline.com
self.addEventListener('fetch', function(event) {
if(event.request.url.indexOf("download-file") !== -1) {
event.respondWith(event.request.formData().then( formdata => {
var response = new Response(formdata.get("filebody"));
response.headers.append('Content-Disposition',
'attachment; filename="' + formdata.get("filename") + '"');
return response;
}));
}
});
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 } });
}());
navigator.serviceWorker.ready.then((registration) => {
registration.periodicSync.register({
tag: 'get-latest-news', // default: ''
minPeriod: 12 * 60 * 60 * 1000, // default: 0
powerState: 'avoid-draining', // default: 'auto'
networkState: 'avoid-cellular' // default: 'online'
}).then((periodicSyncReg) => {
// Successfully registered
})
});
self.addEventListener('periodicsync', function(event) {
if (event.registration.tag == 'get-latest-news') {
event.waitUntil(fetchAndCacheLatestNews());
}
else {
// Unknown sync, may be old, best to unregister
event.registration.unregister();
}
});
const registration = await navigator.serviceWorker.ready;
const bgFetchJob =
await registration.backgroundFetch.fetch(id, requests, options);
addEventListener('backgroundfetched', event => {
event.waitUntil(async function() {
const fetches = await event.fetches.values();
// Put all fetch responses to the cache
...
}());
});
const swReg = await navigator.serviceWorker.register("/sw.js");
await swReg.paymentManager.paymentInstruments.set(
"c8126178-3bba-4d09-8f00-0771bcfd3b11",
{
name: "My Bob Pay Account: john@example.com",
method: "https://bobpay.com",
icons: [{ ... }]
}
);
self.addEventListener("paymentrequest", event => {
// Do the payment flow
// Open window if needed
});