PWA Fundamentals websites with superpowers JS Fest Ukraine 2019 @rowdyrabouw

Доброго ранку! @rowdyrabouw

@rowdyrabouw

Rowdy Raubow @rowdyrabouw

Rowdy Rabouw @rowdyrabouw

@rowdyrabouw

Rowdy Rabouw Born and raised in Gouda, The Netherlands Freelance web and app developer Senior engineer at Nationale-Nederlanden Lead developer Nationale-Nederlanden Pension App Progress Developer Expert for Nativescript @rowdyrabouw rowdy@double-r.nl ❤ superhero movies @rowdyrabouw

@rowdyrabouw

Progressive Web App @rowdyrabouw

Reliable Fast Engaging Always load and never show the downasaur Respond quickly to user interactions Feel like a native app on the device @rowdyrabouw

Key Features • • • • • • • • • Linkable (URL’s) Safe (https) Discoverable (search engines) Responsive (works on every screen resolution) Feel like a native app (fast loading, look-and-feel) Always up-to-date (fresh from the web; no updates from app store) Installable (home screen, cache data) Re-engageable (home screen, push notifications) Connectivity-independent (can work offline) @rowdyrabouw

PWA Progressive Web Apps = ❤ Progressive Modern Browsers Enhancement @rowdyrabouw

Starter Code @rowdyrabouw

<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8” />” <meta name=”viewport” content=”width=device-width, initial-scale=1.0” />” <meta http-equiv=”X-UA-Compatible” content=”ie=edge” />” <title>PWA SuperHeroes</”title> <link rel=”icon” type=”image/png” href=”favicon.png” />” <link rel=”stylesheet” href=”css/app.css” />” </”head> <body> <header> <img src=”img/pwa.svg” alt=”PWA logo” />” <h1>PWA SuperHeroes</”h1> </”header> </”body> </”html>

@rowdyrabouw

@font-face { font-family: “Exo 2”; font-display: swap; font-style: normal; font-weight: 400; src: url(“..”/fonts/exo2.woff2”) format(“woff2”); } body { padding: 20px; } header img { max-height: 100px; } h1 { color: #df2305; font-family: “Exo 2”, sans-serif; margin: 0; text-align: center; } @rowdyrabouw

Manifest File @rowdyrabouw

Manifest File • • • simple JSON file informs the browser about your web application tells it how it should behave when “installed” @rowdyrabouw

{ } “name”: “PWA Superheroes”, “short_name”: “PWAS”, “lang”: “en-GB”, “start_url”: “/”, “scope”: “.”, “display”: “standalone”, “orientation”: “portrait”, “background_color”: “#3D3D3D”, “theme_color”: “#DF2305”, “icons”: [] @rowdyrabouw

<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8” />” <meta name=”viewport” content=”width=device-width, initial-scale=1.0” />” <meta http-equiv=”X-UA-Compatible” content=”ie=edge” />” <title>PWA SuperHeroes</”title> <link rel=”icon” type=”image/png” href=”favicon.png” />” <link rel=”stylesheet” href=”css/app.css” />” <link rel=”manifest” href=”manifest.json” />” </”head> <body> <header> <img src=”img/pwa.svg” alt=”PWA logo” />” <h1>PWA SuperHeroes</”h1> </”header> </”body> </”html>

@rowdyrabouw

{ } “name”: “PWA Superheroes”, “short_name”: “PWAS”, “lang”: “en-GB”, “start_url”: “/”, “scope”: “.”, “display”: “standalone”, “orientation”: “portrait”, “background_color”: “#3D3D3D”, “theme_color”: “#DF2305”, “icons”: [ { “src”: “manifest-icon-192.png”, “sizes”: “192x192”, “type”: “image/png” }, { “src”: “manifest-icon-512.png”, “sizes”: “512x512”, “type”: “image/png” } ] @rowdyrabouw

A2HS @rowdyrabouw

A2HS Criteria • • • the web app is not already installed served over HTTPS web app manifest • • • • • • short_name or name start_url display must be one of: fullscreen, standalone, or minimal-ui icons must include a 192px and a 512px sized icons prefer_related_applications is not true registered service worker with a fetch event handler @rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

@rowdyrabouw

beforeinstallprompt @rowdyrabouw

@rowdyrabouw

@rowdyrabouw

<!DOCTYPE html> <html lang=”en”> <head> …”” <script defer src=”js/a2hs.js”></”script> </”head> <body> <header> <img src=”img/pwa.svg” alt=”PWA logo” />” <h1>PWA SuperHeroes</”h1> </”header> <main> <button id=”btnInstall” class=”hide”>Add to Home screen</”button> </”main> </”body> </”html>

@rowdyrabouw

main { margin-top: 10px; text-align: center; } #btnInstall { color: #ffffff; background-color: #3d3d3d; border-radius: 10px; display: none; padding: 10px 20px; } .show { display: inline-block !important; } @rowdyrabouw

//” a2hs.js let deferredPrompt; const btnInstall = document.getElementById(“btnInstall”); window.addEventListener(“beforeinstallprompt”, event =>” { console.log(“beforeinstallprompt fired”); //” prevent showing dialog event.preventDefault(); deferredPrompt = event; //” show button to install PWA btnInstall.classList.add(“show”); return false; }); @rowdyrabouw

//” a2hs.js btnInstall.addEventListener(“click”, () =>” { if (deferredPrompt) { //” show native dialog deferredPrompt.prompt(); //” act on user choice deferredPrompt.userChoice.then(choiceResult =>” { if (choiceResult.outcome ===”” “dismissed”) { console.log(“User cancelled installation”); } else { console.log(“User added to home screen”); //” hide button to install PWA btnInstall.classList.remove(“show”); deferredPrompt = null; } }); } }); @rowdyrabouw

Service Workers @rowdyrabouw

Service Workers • • • • • • • JavaScript file runs in the background in a separate thread; so non-blocking scoped to a website can’t modify page; no access to DOM asynchronous; promises or async/await event driven @rowdyrabouw

Man-in-the-middle @rowdyrabouw

Marvel-in-the-middle @rowdyrabouw

//” sw.js self.addEventListener(“install”, event =>” { console.log(“[Service Worker] Installing Service Worker …”“”, event); }); self.addEventListener(“activate”, event =>” { console.log(“[Service Worker] Activating Service Worker …”“”, event); //” ensure that the Service Worker is activated correctly (fail-safe) return self.clients.claim(); }); @rowdyrabouw

//” app.js //” check for modern browser if (“serviceWorker” in navigator) { navigator.serviceWorker .register(“/sw.js”) .then(() =>” { console.log(“Service Worker registered!”); }) .catch(err =>” { console.log(err); }); } @rowdyrabouw

<!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8” />” <meta name=”viewport” content=”width=device-width, initial-scale=1.0” />” <meta http-equiv=”X-UA-Compatible” content=”ie=edge” />” <title>PWA SuperHeroes</”title> <link rel=”icon” type=”image/png” href=”favicon.png” />” <link rel=”stylesheet” href=”css/app.css” />” <link rel=”manifest” href=”manifest.json” />” <script src=”js/app.js”></”script> </”head> <body> <header> <img src=”img/pwa.svg” alt=”PWA logo” />” <h1>PWA SuperHeroes</”h1> </”header> </”body> </”html> @rowdyrabouw

Cache API @rowdyrabouw

//” sw.js self.addEventListener(“install”, event =>” { //” without waiting the SW will continue without caching event.waitUntil( //” opens the existing static cache or creates it caches.open(“static”).then(cache =>” { console.log(“[Service Worker] Precaching Files”); cache.add(“/”); cache.add(“index.html”); cache.add(“css/app.css”); cache.add(“fonts/exo2.woff2”); cache.add(“js/app.js”); cache.add(“img/pwa.svg”); cache.add(“favicon.png”); cache.add(“manifest.json”); cache.add(“manifest-icon-192.png”); }) ); }); @rowdyrabouw

//” sw.js const STATIC_CACHE = “static-v1”; const STATIC_FILES = [“/”, “index.html”, “css/app.css”, “fonts/exo2.woff2”, “js/app.js”, “img/pwa.svg”, “favicon.png”, “manifest.json”, “manifest-icon-192.png”]; self.addEventListener(“install”, event =>” { event.waitUntil( caches.open(STATIC_CACHE).then(cache =>” { cache.addAll(STATIC_FILES); }) ); }); @rowdyrabouw

Fetch API @rowdyrabouw

//” sw.js self.addEventListener(“fetch”, event =>” { event.respondWith( //” look at all caches for a match on the key (= request) caches .match(event.request) //” you will always get a response; null if not found .then(response =>” { if (response) { //” if found: return from cache return response; } else { //” fetch it from the server return fetch(event.request); } }) ); }); @rowdyrabouw

швидкість @rowdyrabouw

Cache Cleanup @rowdyrabouw

…”” <main> <button id=”btnInstall” class=”hide”>Add to Home screen</”button> <a href=”about.html”>about</”a> </”main> …”” @rowdyrabouw

//” about.html <!DOCTYPE html> <html lang=”en”> <head> …”” </”head> <body> <header> …”” </”header> <main> <p>This is a demo website a for Progressive Web App talk by Rowdy Rabouw.</”p> <a href=”https://”noti.st/rowdy” target=”_blank”>more info</”a> </”main> </”body> </”html> @rowdyrabouw

a, p { color: #3d3d3d; font-family: “Exo 2”, sans-serif; font-size: 20px; text-decoration-color: #df2305; } @rowdyrabouw

//” sw.js const STATIC_CACHE = “static-v2”; const STATIC_FILES = [“/”, “index.html”, “css/app.css”, “fonts/exo2.woff2”, “js/app.js”, “img/pwa.svg”, “favicon.png”, “manifest.json”, “manifest-icon-192.png”]; @rowdyrabouw

//” sw.js self.addEventListener(“activate”, event =>” { event.waitUntil( //” will return an array of cache names caches.keys().then(keys =>” { return Promise.all( //” go over all available keys keys.map(key =>” { if (key !==”” STATIC_CACHE) { return caches.delete(key); } }) ); }) ); return self.clients.claim(); }); @rowdyrabouw

Dynamic Caching @rowdyrabouw

//” sw.js const DYNAMIC_CACHE = “dynamic-v1”; const STATIC_CACHE = “static-v3”; const STATIC_FILES = [“/”, “index.html”, “css/app.css”, “fonts/exo2.woff2”, “js/app.js”, “js/a2hs.js”, “img/pwa.svg”, “favicon.png”, “manifest.json”, “manifest-icon-192.png”]; @rowdyrabouw

//” sw.js self.addEventListener(“fetch”, event =>” { event.respondWith( caches.match(event.request).then(response =>” { if (response) { return response; } else { } return fetch(event.request); }) ); }); @rowdyrabouw

//” sw.js self.addEventListener(“fetch”, event =>” { event.respondWith( caches.match(event.request).then(response =>” { if (response) { return response; } else { return ( fetch(event.request).then(res =>” { return caches.open(DYNAMIC_CACHE).then(cache =>” { cache.put(event.request.url, res.clone()); return res; }); }) ); } }) ); }); @rowdyrabouw

//” sw.js self.addEventListener(“activate”, event =>” { event.waitUntil( caches.keys().then(keys =>” { return Promise.all( keys.map(key =>” { if (key !==”” STATIC_CACHE) { return caches.delete(key); } }) ); }) ); return self.clients.claim(); }); @rowdyrabouw

//” sw.js self.addEventListener(“activate”, event =>” { event.waitUntil( caches.keys().then(keys =>” { return Promise.all( keys.map(key =>” { if (key !==”” STATIC_CACHE &&” key !==”” CACHE_DYNAMIC_NAME) { return caches.delete(key); } }) ); }) ); return self.clients.claim(); }); @rowdyrabouw

Offline Fallback @rowdyrabouw

//” offline.html <!DOCTYPE html> <html lang=”en”> <head> …”” </”head> <body> <header> …”” </”header> <main> <p><b>We’re sorry, this page hasn’t been cached yet :-(</”b></”p> <p>But why don’t you try one of our <a href=”/”>other pages</”a>?</”p> </”main> </”body> </”html> @rowdyrabouw

//” sw.js const DYNAMIC_CACHE = “dynamic-v1”; const STATIC_CACHE = “static-v4”; const STATIC_FILES = [“/”, “index.html”, “offline.html”, “css/app.css”, “fonts/exo2.woff2”, “js/app.js”, “js/a2hs.js”, “img/pwa.svg”, “favicon.png”, “manifest.json”, “manifest-icon-192.png”]; @rowdyrabouw

//” sw.js self.addEventListener(“fetch”, event =>” { event.respondWith( caches.match(event.request).then(response =>” { if (response) { return response; } else { return ( fetch(event.request).then(res =>” { return caches.open(DYNAMIC_CACHE).then(cache =>” { cache.put(event.request.url, res.clone()); return res; }); }) ); } }) ); }); @rowdyrabouw

//” sw.js …”” return fetch(event.request) .then(res =>” { …”” }) .catch(err =>” { return caches.open(STATIC_CACHE).then(cache =>” { return cache.match(“/offline.html”); }); }); …”” @rowdyrabouw

//” sw.js …”” return fetch(event.request) .then(res =>” { …”” }) .catch(err =>” { return caches.open(STATIC_CACHE).then(cache =>” { if (event.request.headers.get(“accept”).includes(“text/html”)) { return cache.match(“/offline.html”); } }); }); …”” @rowdyrabouw

Application Shell @rowdyrabouw

@rowdyrabouw

…”” <main> <button id=”btnInstall” class=”hide”>Add to Home screen</”button> <a href=”about.html”>about</”a> <div class=”container”> <div class=”placeholder”> <h2 id=”name”> </”h2> </”div> <div class=”placeholder”> <p id=”details”> <br />” </”p> </”div> <div class=”placeholder img-placeholder” id=”image”></”div> </”div> </”main> …”” @rowdyrabouw

h2 { font-family: “Exo 2”, sans-serif; margin-bottom: 0; } .container { width: 240px; margin: auto; } .placeholder { background-color: #f3f3f3; margin-bottom: 10px; animation: colorchange 2.5s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; animation-iteration-count: infinite; } @rowdyrabouw

.img-placeholder { height: 320px; } img { max-width: 100%; border-top-left-radius: 70px; border-bottom-right-radius: 70px; } @keyframes colorchange { 0% { background: #f3f3f3; } 50% { background: #d1d1d1; } 100% { background: #f3f3f3; } } @rowdyrabouw

//” sw.js const STATIC_CACHE = “static-v5”; @rowdyrabouw

Dynamic Data @rowdyrabouw

//” api.js const url = “https://”www.superheroapi.com/api.php/3251184438288441/213”; fetch(url) .then(res =>” { return res.json(); }) .then(data =>” { console.log(“Retrieved from web”, data); updateUI(“web”, data); }); @rowdyrabouw

//” api.js function updateUI(updateBy, data) { console.log(Update UI by ${updateBy}); document.getElementById(“name”).innerHTML = data.name; const details = ${data.biography["full-name"]}, ${data.biography["placeof-birth"]}<br/>"${data.appearance.height[1]} / ${data.appearance.weight[1]}; document.getElementById(“details”).innerHTML = details; document.getElementById(“image”).innerHTML = <img src="$ {data.image.url}" alt="${data.name}" />"; const all = document.getElementsByClassName(“placeholder”); for (let i = 0; i < all.length; i++”) { all[i].style.backgroundColor = “transparent”; all[i].style.animation = “none”; } } @rowdyrabouw

<!DOCTYPE html> <html lang=”en”> <head> …”” <script defer src=”js/api.js”></”script> </”head> <body> …”” </”body> </”html>

@rowdyrabouw

//” sw.js const STATIC_CACHE = “static-v6”; const STATIC_FILES = [“/”, “index.html”, “offline.html”, “css/app.css”, “fonts/exo2.woff2”, “js/app.js”, “js/a2hs.js”, “js/api.js”, “img/pwa.svg”, “favicon.png”, “manifest.json”, “manifest-icon-192.png”]; @rowdyrabouw

IndexedDB @rowdyrabouw

IndexedDB An asynchronous transactional key-value database in the browser for data that changes frequently and is typically in JSON format. Transactional means that if one of the actions within an operation fails, none of those actions are applied to keep database integrity. @rowdyrabouw

IndexedDB An asynchronous transactional key-value database in the browser for data that changes frequently and is typically in JSON format. Transactional means that if one of the actions within an operation fails, none of those actions are applied to keep database integrity. @rowdyrabouw

<!DOCTYPE html> <html lang=”en”> <head> …”” <script src=”idb.js”></”script> <script src=”utils.js”></”script> </”head> <body> …”” </”body> </”html>

@rowdyrabouw

//” sw.js importScripts(“idb.js”); importScripts(“utils.js”); const DYNAMIC_CACHE = “dynamic-v1”; const STATIC_CACHE = “static-v7”; const STATIC_FILES = [“/”, “index.html”, “offline.html”, “css/app.css”, “idb.js”, “utils.js”, “fonts/exo2.woff2”, “js/app.js”, “js/a2hs.js”, “js/api.js”, “img/pwa.svg”, “favicon.png”, “manifest.json”, “manifest-icon-192.png”]; @rowdyrabouw

//” utils.js const DYNAMIC_DB = “dynamic”; const DYNAMIC_DB_VERSION = 1; const DYNAMIC_DB_STORE = “heroes”; const API_URL = “https://”www.superheroapi.com/api.php/3251184438288441/213”; //” open database and get a callback after it is opened const IDB = idb.open(DYNAMIC_DB, DYNAMIC_DB_VERSION, db =>” { //” create a store if it doesn’t exist already if (!db.objectStoreNames.contains(DYNAMIC_DB_STORE)) { //” name the store and set the primary key db.createObjectStore(DYNAMIC_DB_STORE, { keyPath: “id” }); } }); @rowdyrabouw

//” utils.js function writeData(storeName, data) { console.log(“Write data to cache”, data); return IDB.then(db =>” { const tx = db.transaction(storeName, “readwrite”); const store = tx.objectStore(storeName); store.put(data); return tx.complete; }); } @rowdyrabouw

//” utils.js function clearAllData(storeName) { return IDB.then(db =>” { const tx = db.transaction(storeName, “readwrite”); const store = tx.objectStore(storeName); store.clear(); return tx.complete; }); } @rowdyrabouw

//” utils.js function readAllData(storeName) { return IDB.then(db =>” { const tx = db.transaction(storeName, “readonly”); const store = tx.objectStore(storeName); return store.getAll(); }); } @rowdyrabouw

//” sw.js self.addEventListener(“fetch”, event =>” { if (event.request.url.indexOf(API_URL) > -1) { event.respondWith( fetch(event.request).then(res =>” { const clonedRes = res.clone(); clearAllData(DYNAMIC_DB_STORE) .then(() =>” { return clonedRes.json(); }) .then(data =>” { writeData(DYNAMIC_DB_STORE, data); }); return res; }) ); } …”” @rowdyrabouw

//” sw.js self.addEventListener(“fetch”, event =>” { if (event.request.url.indexOf(API_URL) > -1) { …”” } else { event.respondWith( caches.match(event.request).then(response =>” { …”” }) ); } }); @rowdyrabouw

//” api.js const url = “https://”www.superheroapi.com/api.php/3251184438288441/213”; let networkDataReceived = false; fetch(API_URL) .then(res =>” { return res.json(); }) .then(data =>” { networkDataReceived = true; console.log(“Retrieved from web”, data); updateUI(“web”, data); }); @rowdyrabouw

//” api.js readAllData(DYNAMIC_DB_STORE).then(data =>” { if (!networkDataReceived) { console.log(“Retrieved from cache”, data[0]); if (typeof data[0] !==”” “undefined”) { updateUI(“cache”, data[0]); } } }); @rowdyrabouw

https://developers.google.com/web/tools/workbox @rowdyrabouw

Background Sync API @rowdyrabouw

Background Sync API • • defer actions until the user has stable connectivity ensure that whatever the user wants to send is sent when they regain connectivity • • behaves similar to the outbox on an email client messages are queued up in the outbox (indexedDB) and as soon as there’s a connection, they’re sent • make sure to notify the user @rowdyrabouw

Periodic Sync API @rowdyrabouw

Periodic Sync API • • sync data on a regular basis; update the PWA • PeriodicSync API will take into account the battery level and while the user is sleeping for instance! network state of the device it’s running on • • powerState: ‘auto’ or ‘avoid-draining’ networkState: ‘avoid-cellular’ @rowdyrabouw

Periodic Sync API E R U T A E F L • • sync data on a regular basis; update the PWA • PeriodicSync API will take into account the battery level and while the user is sleeping for instance! A T N E network state of the device it’s running on • • M I R powerState: ‘auto’ or ‘avoid-draining’ E P networkState: ‘avoid-cellular’ X E @rowdyrabouw

New version available! @rowdyrabouw

<div id=”snackbar”>A new version of this app is available. Click here to update.</”div> @rowdyrabouw

#snackbar { visibility: hidden; background-color: #e02205; color: #ffffff; text-align: center; padding: 16px; position: fixed; z-index: 1; left: 0; top: 0; font-family: “Exo 2”, sans-serif; font-size: 20px; border: 3px solid #000000; } #snackbar.show { visibility: visible; } @rowdyrabouw

//” sw.js const STATIC_CACHE = “static-v8”; self.addEventListener(“message”, event =>” { if (event.data.action ===”” “skipWaiting”) { self.skipWaiting(); } }); @rowdyrabouw

//” app.js let newWorker; function showUpdateBar() { let snackbar = document.getElementById(“snackbar”); snackbar.className = “show”; } document.getElementById(“snackbar”).addEventListener(“click”, () =>” { newWorker.postMessage({ action: “skipWaiting” }); }); @rowdyrabouw

//” app.js navigator.serviceWorker .register(“/sw.js”) .then(reg =>” { reg.addEventListener(“updatefound”, () =>” { newWorker = reg.installing; newWorker.addEventListener(“statechange”, () =>” { switch (newWorker.state) { case “installed”: if (navigator.serviceWorker.controller) { showUpdateBar(); } break; } }); }); }) @rowdyrabouw

//” app.js navigator.serviceWorker.addEventListener(“controllerchange”, () =>” { window.location.reload(); }); @rowdyrabouw

https://noti.st/rowdy https://github.com/rowdyrabouw/pwa-superheroes.com @rowdyrabouw

Rowdy Rabouw Born and raised in Gouda, The Netherlands Freelance web and app developer Senior engineer at Nationale-Nederlanden Lead developer Nationale-Nederlanden Pension App Progress Developer Expert for Nativescript @rowdyrabouw rowdy@double-r.nl ❤ superhero movies @rowdyrabouw