Maxim Salnikov
@webmaxru
What did we learn from
3 years of exploring PWA idea?
June 15, 2015
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
OS
Service Worker
API
Web App Manifest
Web App
Service worker
Browser / WebView
Event-driven worker
'install'
Parsed
Installing
Activating
Redundant
'activate'
Waiting
Active
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));
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(cacheName).then((cache) => {
return cache.addAll(appShellFilesToCache)
})
)
})
const appShellFilesToCache = [
...
'./assets/redirect/redirectfrom.html'
]
app.get('/assets/redirect/redirectfrom.html', (req, res) => {
res.redirect(301, '/assets/redirect/redirectto.html')
})
// If "cleanRedirects" and this is a redirected response,
// then get a "clean" copy to add to the cache.
const newResponse = cleanRedirects && response.redirected ?
await cleanResponseCopy({response}) :
response.clone();
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
const env = {
// Environment polyfills
skipWaiting: Function,
caches: CacheStorage,
clients: Clients,
registration: ServiceWorkerRegistration,
addEventListener: Function,
Request: constructor Function,
Response: constructor Function,
URL: constructor Function
};
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');
})
});
window.addEventListener("appinstalled", event => {
console.log('The app was installed')
// 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 } });
}());
SW Boot
Navigation request
SW Boot
Navigation request
self.addEventListener('activate', e => {
e.waitUntil(self.registration.navigationPreload.enable());
});
self.addEventListener('fetch', event => {
event.respondWith(async function() {
// Respond from the cache if we can
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
// Else, use the preloaded response, if it's there
const response = await event.preloadResponse;
if (response) return response;
// Else try the network.
return fetch(event.request);
}());
});
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
});