Smartphones: | 700 Million |
---|---|
Total Web: | 3,700 Million |
1 | 640x360 | 16.18% |
2 | 1920x1080 | 11.24% |
3 | 1366x768 | 9.68% |
4 | 1024x768 | 6.46% |
5 | 667x375 | 5.69% |
6 | 800x600 | 3.36% |
7 | 720x360 | 3.27% |
8 | 760x360 | 3.10% |
9 | 1440x900 | 2.71% |
10 | 736x414 | 2.30% |
beforeinstallprompt
prompt
appinstalled
Average of 0 per month for last 5 years
60% of apps in the Play Store have never been downloaded
3/4 of users who start the install process don't finish it
auto-creation of a WebAPK
auto-creation and Windows Store install / uninstall
...no store support! Waa waa waaah :(
Safari does allow PWA installs, but won't promote
(144 x 144 minimum)
<head>
<link rel="manifest" href="/manifest.json" />
</head>
Note that "/manifest.json" is requested without any credentials, even on the same domain. If you require credentials:
<head>
<link rel="manifest" href="/manifest.json"
cossorigin="use-credentials" />
</head>
{
"name": "Google Maps",
"icons": [
{
"src": "/images/icons-192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/images/icons-512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/maps/?source=pwa",
}
Web Workers |
Service Workers | |
---|---|---|
Tab control | Many per tab | One for all tabs |
Lifespan | Same as tab | Independent |
Good for | Parallelism | Offline |
//check for functionality
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js')
.then(function(reg) {
// Registration was successful
console.log('Registered with: ', reg.scope);
}, function(err) {
// registration failed :(
console.error('Registration failed: ', err);
});
});
}
/sw.js has root scope
/subarea/sw.js matches:
/subarea/page1
/subarea/foo/bar
/other
push
fetch
Fetch API
Server Sent Events (SSE / EventSource)
Web Sockets
Local Storage
Session Storage
Cache
IndexedDB
- Precaching - Runtime caching - Pre-built Strategies - Request routing - Offline Analytics |
- Background sync - Helpful debugging - Successor to sw-precache and sw-toolbox |
---|
https://developers.google.com/web/fundamentals/
instant-and-offline/offline-cookbook/
<link rel="manifest" href="/manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Weather PWA">
<link rel="apple-touch-icon" href="/images/icons/icon-152x152.png">
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then((reg) => {
console.log('Service worker registered.', reg);
});
});
}
const FILES_TO_CACHE = [
'/offline.html',
];
evt.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[ServiceWorker] Pre-caching offline page');
return cache.addAll(FILES_TO_CACHE);
})
);
const CACHE_NAME = 'mycache.v2'
evt.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if (key !== CACHE_NAME) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
if (evt.request.mode !== 'navigate') { //bail
return;
}
evt.respondWith(
fetch(evt.request)
.catch(() => {
return caches.open(CACHE_NAME)
.then((cache) => {
return cache.match('offline.html');
});
})
);
// function getForecastFromCache()
const url = `${window.location.origin}/forecast/${coords}`;
return caches.match(url)
.then((response) => {
if (response) return response.json();
else return null;
}).catch((err) => {
console.error('Error getting data from cache', err);
return null;
});
// within updateData:
getForecastFromCache(location.geo)
.then((forecast) => {
renderForecast(card, forecast);
});
getForecastFromNetwork(location.geo)
.then((forecast) => {
renderForecast(card, forecast);
});
const CACHE_NAME = 'static-cache-v2'; //note updated name
const DATA_CACHE_NAME = 'data-cache-v1'; //new dynamic cache
const FILES_TO_CACHE = [
// root files and core scripts
'/',
'/index.html',
'/scripts/app.js',
'/scripts/install.js',
'/scripts/luxon-1.11.4.js',
'/styles/inline.css',
// common images
'/images/add.svg',
'/images/clear-day.svg',
'/images/clear-night.svg',
...
];
if (evt.request.url.includes('/forecast/')) { //data API calls
console.log('[Service Worker] Fetch (data)', evt.request.url);
evt.respondWith(
caches.open(DATA_CACHE_NAME).then((cache) => {
return fetch(evt.request)
.then((response) => {
// Clone & cache good results
if (response.status === 200) {
cache.put(evt.request.url, response.clone());
}
return response;
}).catch((err) => {
// Network request failed, try the cache.
return cache.match(evt.request);
});
}));
return;
} //else static asset; return cached or request if null
<script src="/scripts/install.js"></script>
window.addEventListener('beforeinstallprompt', saveInstallPrompt);
// function installButton.onClick()
deferredInstallPrompt.prompt();
evt.srcElement.setAttribute('hidden', true); //hide after install
// function saveInstallPrompt()
deferredInstallPrompt = evt;
installButton.removeAttribute('hidden');
deferredInstallPrompt.userChoice
.then((choice) => {
if (choice.outcome === 'accepted') { // installed
console.log('Unicorns and kittens for all!', choice);
} else { // not installed
console.log('Release the hounds!', choice);
}
deferredInstallPrompt = null;
});
window.addEventListener('appinstalled', (installEvt) => {
console.log("Magically delicious!", installEvt) });
// CSS
@media all and (display-mode: standalone) {
body {
background-color: yellow;
}
}
// JS
if (window.matchMedia('(display-mode: standalone)').matches) {
console.log('display-mode is standalone');
}
// JS Safari
if (window.navigator.standalone === true) {
console.log('display-mode is standalone');
}
// Ask the user for permissions
Notification.requestPermission((status) => {
console.log('Notification permission status:', status);
});
// Show a notification
const btn = document.getElementById('btn');
btn.addEventListener('click', () => {
if (Notification.permission == 'granted') {
navigator.serviceWorker.getRegistration()
.then(reg => reg.showNotification('Hello world!'))
}
})