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.
Flagged
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);
});
});
});
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');
})
});
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 } });
}());