Skip to content

Instantly share code, notes, and snippets.

@dr-dimitru
Last active October 30, 2019 12:07
Show Gist options
  • Save dr-dimitru/d26df3a15edb866f44c7f72dc972d856 to your computer and use it in GitHub Desktop.
Save dr-dimitru/d26df3a15edb866f44c7f72dc972d856 to your computer and use it in GitHub Desktop.
Installable PWA 101
# Add proper mime-type in Apache web server configuration
AddType application/manifest+json webmanifest

Installable PWA 101

Instructions are coming soon

<?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>
<button type="button" onclick="fullAppRefresh()">Refresh Web Application</button>
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);
}
<!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>
// 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
}
<!-- Show this button on Android (iOS will be supported very soon) -->
<button type="button" onclick="installPWA()">Install PWA</button>
function installPWA () {
if (swInstallPrompt) {
swInstallPrompt.prompt();
swInstallPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
// Hooray! User accepted install prompt
} else {
alert('We\'re sorry, PWA can\'t be installed now 😕. PWA standard is very young and implemented differently from browser to browser, we\'re working hard to add PWA support to all platforms.');
}
}).catch((error) => {
console.error('[swInstallPrompt.userChoice.catch()] ERROR:', error);
});
}
swInstallPrompt = null;
}
{
"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"
}
# 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;
}
}
// 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