PWA Fundamentals: websites with superpowers

A presentation at JS Fest Ukraine in November 2019 in Kyiv, Ukraine, 02000 by Rowdy Rabouw

Slide 1

Slide 1

PWA Fundamentals websites with superpowers JS Fest Ukraine 2019 @rowdyrabouw

Slide 2

Slide 2

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

Slide 3

Slide 3

@rowdyrabouw

Slide 4

Slide 4

Rowdy Raubow @rowdyrabouw

Slide 5

Slide 5

Rowdy Rabouw @rowdyrabouw

Slide 6

Slide 6

@rowdyrabouw

Slide 7

Slide 7

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

Slide 8

Slide 8

@rowdyrabouw

Slide 9

Slide 9

Progressive Web App @rowdyrabouw

Slide 10

Slide 10

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

Slide 11

Slide 11

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

Slide 12

Slide 12

Slide 13

Slide 13

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

Slide 14

Slide 14

Starter Code @rowdyrabouw

Slide 15

Slide 15

<!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

Slide 16

Slide 16

@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

Slide 17

Slide 17

Slide 18

Slide 18

Manifest File @rowdyrabouw

Slide 19

Slide 19

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

Slide 20

Slide 20

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

Slide 21

Slide 21

<!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

Slide 22

Slide 22

Slide 23

Slide 23

Slide 24

Slide 24

Slide 25

Slide 25

{ } “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

Slide 26

Slide 26

Slide 27

Slide 27

A2HS @rowdyrabouw

Slide 28

Slide 28

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

Slide 29

Slide 29

@rowdyrabouw

Slide 30

Slide 30

@rowdyrabouw

Slide 31

Slide 31

@rowdyrabouw

Slide 32

Slide 32

@rowdyrabouw

Slide 33

Slide 33

@rowdyrabouw

Slide 34

Slide 34

@rowdyrabouw

Slide 35

Slide 35

@rowdyrabouw

Slide 36

Slide 36

@rowdyrabouw

Slide 37

Slide 37

@rowdyrabouw

Slide 38

Slide 38

@rowdyrabouw

Slide 39

Slide 39

beforeinstallprompt @rowdyrabouw

Slide 40

Slide 40

@rowdyrabouw

Slide 41

Slide 41

@rowdyrabouw

Slide 42

Slide 42

<!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

Slide 43

Slide 43

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

Slide 44

Slide 44

//” 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

Slide 45

Slide 45

//” 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

Slide 46

Slide 46

Slide 47

Slide 47

Slide 48

Slide 48

Service Workers @rowdyrabouw

Slide 49

Slide 49

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

Slide 50

Slide 50

Man-in-the-middle @rowdyrabouw

Slide 51

Slide 51

Marvel-in-the-middle @rowdyrabouw

Slide 52

Slide 52

//” 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

Slide 53

Slide 53

//” 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

Slide 54

Slide 54

<!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

Slide 55

Slide 55

Slide 56

Slide 56

Cache API @rowdyrabouw

Slide 57

Slide 57

Slide 58

Slide 58

//” 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

Slide 59

Slide 59

//” 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

Slide 60

Slide 60

Slide 61

Slide 61

Slide 62

Slide 62

Fetch API @rowdyrabouw

Slide 63

Slide 63

Slide 64

Slide 64

//” 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

Slide 65

Slide 65

Slide 66

Slide 66

швидкість @rowdyrabouw

Slide 67

Slide 67

Slide 68

Slide 68

Slide 69

Slide 69

Cache Cleanup @rowdyrabouw

Slide 70

Slide 70

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

Slide 71

Slide 71

//” 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

Slide 72

Slide 72

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

Slide 73

Slide 73

//” 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

Slide 74

Slide 74

Slide 75

Slide 75

//” 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

Slide 76

Slide 76

Dynamic Caching @rowdyrabouw

Slide 77

Slide 77

//” 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

Slide 78

Slide 78

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

Slide 79

Slide 79

//” 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

Slide 80

Slide 80

//” 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

Slide 81

Slide 81

//” 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

Slide 82

Slide 82

Slide 83

Slide 83

Offline Fallback @rowdyrabouw

Slide 84

Slide 84

Slide 85

Slide 85

//” 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

Slide 86

Slide 86

//” 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

Slide 87

Slide 87

//” 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

Slide 88

Slide 88

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

Slide 89

Slide 89

//” 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

Slide 90

Slide 90

Slide 91

Slide 91

Application Shell @rowdyrabouw

Slide 92

Slide 92

@rowdyrabouw

Slide 93

Slide 93

…”” <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

Slide 94

Slide 94

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

Slide 95

Slide 95

.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

Slide 96

Slide 96

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

Slide 97

Slide 97

Slide 98

Slide 98

Dynamic Data @rowdyrabouw

Slide 99

Slide 99

//” 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

Slide 100

Slide 100

//” 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

Slide 101

Slide 101

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

@rowdyrabouw

Slide 102

Slide 102

//” 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

Slide 103

Slide 103

Slide 104

Slide 104

IndexedDB @rowdyrabouw

Slide 105

Slide 105

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

Slide 106

Slide 106

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

Slide 107

Slide 107

Slide 108

Slide 108

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

@rowdyrabouw

Slide 109

Slide 109

//” 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

Slide 110

Slide 110

//” 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

Slide 111

Slide 111

//” 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

Slide 112

Slide 112

//” 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

Slide 113

Slide 113

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

Slide 114

Slide 114

//” 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

Slide 115

Slide 115

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

Slide 116

Slide 116

//” 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

Slide 117

Slide 117

//” 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

Slide 118

Slide 118

Slide 119

Slide 119

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

Slide 120

Slide 120

Background Sync API @rowdyrabouw

Slide 121

Slide 121

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

Slide 122

Slide 122

Slide 123

Slide 123

Periodic Sync API @rowdyrabouw

Slide 124

Slide 124

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

Slide 125

Slide 125

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

Slide 126

Slide 126

Slide 127

Slide 127

New version available! @rowdyrabouw

Slide 128

Slide 128

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

Slide 129

Slide 129

#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

Slide 130

Slide 130

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

Slide 131

Slide 131

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

Slide 132

Slide 132

//” 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

Slide 133

Slide 133

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

Slide 134

Slide 134

Slide 135

Slide 135

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

Slide 136

Slide 136

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