Instructions are coming soon
Last active
October 30, 2019 12:07
-
-
Save dr-dimitru/d26df3a15edb866f44c7f72dc972d856 to your computer and use it in GitHub Desktop.
Installable PWA 101
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Add proper mime-type in Apache web server configuration | |
AddType application/manifest+json webmanifest |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8" ?> | |
<!-- Add mime-type in IIS web server configuration --> | |
<configuration> | |
<system.webServer> | |
<staticContent> | |
<mimeMap fileExtension="webmanifest" mimeType="application/manifest+json"/> | |
</staticContent> | |
</system.webServer> | |
</configuration> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<button type="button" onclick="fullAppRefresh()">Refresh Web Application</button> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function fullAppRefresh () { | |
// Purge applicationCache: step 1 | |
try { | |
window.applicationCache.swapCache(); | |
} catch (_error) { | |
// We good here... | |
} | |
// Purge applicationCache: step 2 | |
try { | |
window.applicationCache.update(); | |
} catch (_error) { | |
// We good here... | |
} | |
// Purge CacheStorage | |
try { | |
window.caches.keys().then((keys) => { | |
keys.forEach((name) => { | |
window.caches.delete(name); | |
}); | |
}).catch((err) => { | |
console.error('window.caches.delete', err); | |
}); | |
} catch (_error) { | |
// We good here... | |
} | |
// Unregister ServiceWorker | |
if (swRegistration) { | |
try { | |
swRegistration.unregister().catch((err) => { | |
console.warn('[SW UNREGISTER] [CATCH IN PROMISE] [ERROR:]', err); | |
}); | |
swRegistration = null; | |
} catch (err) { | |
console.warn('[SW UNREGISTER] [ERROR:]', err); | |
} | |
} | |
// Refresh the page, using .setTimeout() | |
// to make sure page refreshed in the separate event-loop | |
setTimeout(() => { | |
if (window.location.hash || window.location.href.endsWith('#')) { | |
// Refresh the page via .reload() if URI hash is presented | |
// and we wish to keep it as it is | |
window.location.reload(); | |
} else { | |
// Refresh the page via .replace() | |
window.location.replace(window.location.href); | |
} | |
}, 128); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"/> | |
<meta name="fragment" content="!"/> | |
<meta name="viewport" content="width=device-width, initial-scale=1"/> | |
<title>Installable PWA</title> | |
<!-- rel="manifest" is only required link tag for PWA, everything above is recommended --> | |
<link rel="manifest" href="/manifest.webmanifest"/> | |
</head> | |
<body></body> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// These variables should be available in other files across the application | |
// Usually this made by having single `app` Object exported/imported in almost every file | |
// This is simplified codebase version, let's assume these varaibles global or all code placed in the single file | |
let swRegistration = null; | |
let swInstallPrompt = null; | |
let hasPWASupport = false; | |
try { | |
if ('serviceWorker' in navigator) { | |
window.addEventListener('beforeinstallprompt', (event) => { | |
hasPWASupporttrue = true; | |
swInstallPrompt = event; | |
}); | |
window.addEventListener('load', () => { | |
try { | |
navigator.serviceWorker.register('/full/path/to/sw.js').then((registration) => { | |
swRegistration = registration; | |
}).catch((error) => { | |
console.info('Can\'t load SW'); | |
console.error(error); | |
}); | |
} catch (e) { | |
// We're good here | |
// Just an old browser | |
} | |
}); | |
} | |
} catch (e) { | |
// We're good here | |
// Just an old browser | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "Application Name", | |
"lang": "en", | |
"short_name": "Title", | |
"description": "Application Short Description", | |
"icons": [ | |
{ | |
"src": "/android-chrome-36x36.png", | |
"sizes": "36x36", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-48x48.png", | |
"sizes": "48x48", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-72x72.png", | |
"sizes": "72x72", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-96x96.png", | |
"sizes": "96x96", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-144x144.png", | |
"sizes": "144x144", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-192x192.png", | |
"sizes": "192x192", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-256x256.png", | |
"sizes": "256x256", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-384x384.png", | |
"sizes": "384x384", | |
"type": "image/png" | |
}, | |
{ | |
"src": "/android-chrome-512x512.png", | |
"sizes": "512x512", | |
"type": "image/png" | |
} | |
], | |
"theme_color": "#ffffff", | |
"background_color": "#ffffff", | |
"start_url": "/", | |
"display": "standalone", | |
"orientation": "portrait" | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# To properly execute .webmanifest file | |
# It's got to be sent with proper mime-type | |
# Here's an example for nginx configuration file | |
http { | |
# Mime-Types usually located in /etc/nginx/mime.types | |
# and imported at nginx.conf via: | |
# import /etc/nginx/mime.types; | |
types { | |
application/manifest+json webmanifest; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Use old-style and error-prone | |
// "Immediately-Invoked Function Expression" (IIFE) | |
// It's not necessary, but it's a good practice, | |
// althought ServiceWorkers very well sandboxed | |
;(function (self) { | |
'use strict'; | |
// Can be set to any String | |
// Increment version each time changes introduced to chached files or sw.js itself | |
const CACHE_NAME = 'cache_version_key_v1'; | |
// Cache static files on initial ServiceWorker load | |
const pages = ['/', '/fonts/font.woff2', '/images/img.jpeg', '/manifest.webmanifest']; | |
const origin = self.location.origin; | |
const RE = { | |
html: /text\/html/i, | |
method: /GET/i, // Proxy-pass and cache only GET HTTP requests | |
static: /\.(?:html|png|jpe?g|ico|css|js|gif|webm|webp|eot|svg|ttf|webmanifest|woff|woff2)(?:\?[a-zA-Z0-9-._~:\/#\[\]@!$&\'()*+,;=]*)?$/i, // Do not proxy-pass requests to static files | |
staticVendors: /(?:fonts\.googleapis\.com|gstatic\.com)/i, // Do not proxy-pass requests to static assets vendors | |
sockjs: /\/sockjs\// // Do not proxy-pass requests to SockJS | |
}; | |
// Handle request exception and return | |
// [503: Service Unavailable] response | |
const exceptionHandler = (error) => { | |
console.error('[ServiceWorker] [exceptionHandler] Network Error:', error); | |
return new Response('<html><body><h1>Service Unavailable</h1><a href="#" onClick="window.location.href=window.location.href">Reload</a></body></html>', { | |
status: 503, | |
statusText: 'Service Unavailable', | |
headers: new Headers({ | |
'Content-Type': 'text/html' | |
}) | |
}); | |
}; | |
// Try to return cached request | |
// If request not found in the cache | |
// return handled exception [503] | |
const cacheOrException = (req, error) => { | |
if (RE.html.test(req.headers.get('accept'))) { | |
return caches.match('/').then((cached) => { | |
return cached || exceptionHandler(error); | |
}); | |
} | |
return exceptionHandler(error); | |
}; | |
// Validate request for caching and proxy-passing via ServiceWorker | |
// Valid only if: [HTTP/GET] && Not [SockJS] && has No [Range] request header | |
const requestCheck = (req) => { | |
return RE.method.test(req.method) && !RE.sockjs.test(req.url) && !req.headers.get('Range'); | |
}; | |
// Validate request origin | |
const originStaticCheck = (req) => { | |
return req.url === origin || req.url === `${origin}/` || (req.url.startsWith(origin) && RE.static.test(req.url)); | |
}; | |
// Check if request is sent to static file | |
const vendorStaticCheck = (req) => { | |
return RE.staticVendors.test(req.url) && RE.static.test(req.url); | |
}; | |
// Handle Install event | |
self.addEventListener('install', (event) => { | |
event.waitUntil(caches.open(CACHE_NAME).then((cache) => { | |
return cache.addAll(pages); | |
}).then(self.skipWaiting())); | |
}); | |
// Handle fetch event (e.g. HTTP request) | |
self.addEventListener('fetch', (event) => { | |
// Check if request should be handled with ServiceWorker | |
if (requestCheck(event.request) && (originStaticCheck(event.request) || vendorStaticCheck(event.request))) { | |
const req = event.request.clone(); | |
// Check if request exists in the cache | |
event.respondWith(caches.match(req).then((cached) => { | |
// Even if request/response is already cached | |
// we will send request and update the cache | |
const fresh = fetch(req).then((response) => { | |
if (response && response.status === 200 && response.type === 'basic') { | |
const resp = response.clone(); | |
caches.open(CACHE_NAME).then((cache) => { | |
cache.put(req, resp); | |
}); | |
} | |
return response || cached || cacheOrException(req, `can't reach a server: ${req.url}`); | |
}).catch((error) => { | |
return cacheOrException(req, error); | |
}); | |
// Return cached response (if exists in the cache) | |
// Return fresh response if it's first-time request | |
return cached || fresh; | |
})); | |
} | |
}); | |
// Handle ServiceWorker activation | |
// Check if current CACHE_NAME matching existing cache | |
// if not — purge the cache | |
self.addEventListener('activate', (event) => { | |
event.waitUntil(caches.keys().then((cacheNames) => { | |
return Promise.all(cacheNames.filter((cacheName) => { | |
return CACHE_NAME !== cacheName; | |
}).map((cacheName) => { | |
return caches.delete(cacheName).catch(() => {}); | |
})); | |
}).then(() => self.clients.claim())); | |
}); | |
})(this); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment