Marko Knöbl
Code verfügbar unter: https://github.com/marko-knoebl/courses-code
npm-Paket http-server
npm install -g http-server
http-server
https://developers.google.com/web/ilt/pwa/why-build-pwa
Service worker:
https://caniuse.com/#search=service%20workers
Web app manifest:
https://caniuse.com/#search=manifest
indexedDB:
Entwicklerwerkzeuge - audits
Service Worker sind ein Kernelement von PWAs. Sie dienen als lokaler Proxy zwischen dem Web Browser und dem Server.
Haupteinsatzgebiet: Offlinenutzung / schnellere Nutzung von Web Apps (Ersatz für die veraltete AppCache-Funktion)
Endgerät ⟺ Web Server
Endgerät ⟺ Service Worker ⟺ Web Server
service worker = Skript, das im Hintergrund läuft
Funktionen:
Wir rufen .register()
auf und übergeben den Pfad des Service Worker Skripts
// main.js
navigator.serviceWorker.register('./serviceWorker.js');
// serviceWorker.js
console.log('this is the service worker');
Chrome: developer tools (F12) âž¡ Application âž¡ Service Workers
Firefox: gehe zu about:debugging âž¡ dieser Firefox âž¡ Service Worker
Workbox = Library, die das Schreiben von Serviceworkern erleichtert
Einbinden eines Service Workers der Antworten cacht und sie als Fallback verwendet, falls Resourcen nicht verfügbar sind:
// service-worker.js
importScripts(
'https://storage.googleapis.com/' +
'workbox-cdn/releases/4.1.1/workbox-sw.js'
);
workbox.routing.registerRoute(
new RegExp('.*'),
new workbox.strategies.NetworkFirst()
);
Wir können die Auswirkungen der Verwendung von Service Workern in den Chrome Developer Tools unter Application/Service Workers und Application/Cache Storage begutachten
Das web app manifest ist eine json Datei, die Informationen zu einer Webanwendung beinhaltet.
Durch Bereitstellung einer manifest Datei kann die Installation einer PWA ermöglicht werden.
einbinden via:
<link rel="manifest" href="manifest.json" />
{
"name": "Todo",
"short_name": "Todo",
"start_url": ".",
"display": "standalone",
"icons": [
{
"src": "images/icon-32.png",
"sizes": "32x32",
"type": "image/png"
},
...
]
}
essentielle Einträge in Chrome:
name
short_name
start_url
icons
- verwendet im Menü, im Splash Screen; für Chrome sollten icons der folgenden Größen bereitgestellt werden: 144
, 192
, 512
display
: fullscreen
/ standalone
/ minimal-ui
/ browser
background_color
- sollte die gleiche Farbe sein wie die CSS-Hintergrundfarbe der Anwendungdescription
orientation
:
any
natural
landscape
(landscape-primary
, landscape-secondary
)portrait
(portrait-primary
,
portrait-secondary
)theme_color
Diese Mata Tags sind hilfreich:
<meta name="theme-color" content="..." />
- das sollte das gleiche sein wie theme_color
in der Manifest-Datei<meta name="apple-mobile-web-app-capable" content="yes">
- versteckt das Browser UIBrowser können die Möglichkeit bieten, für PWAs Einträge zum Startmenü / zum Homescreen hinzuzufügen
Unter iOS können Benutzer einen Shortcut zu jeder Website zum Menü hinzufügen. Für PWAs funktioniert das auf die gleiche Art.
Bei Chrome können PWAs den Benutzer zur Installation auffordern. Installierte PWAs verhalten sich anders als Webseiten - z.B. erscheinen sie in einem seperaten Fenster.
für Chrome:
https://developers.google.com/web/fundamentals/app-install-banners/
Voraussetzung, um den App-Installations-Dialog anzuzeigen:
Sobald alle Voraussetzungen erfüllt sind, wird das beforeinstallprompt
Event ausgelöst; Wir können dieses Event abfangen und für später speichern
let installPromptEvent;
window.addEventListener(
'beforeinstallprompt',
(ipEvent) => {
// the browser is ready to show the install prompt
ipEvent.preventDefault();
installPromptEvent = ipEvent;
showInstallBtn();
}
);
Sobald der Benutzer die Anwendung installieren möchte können wir das gespeicherte Event verwenden:
installBtn.addEventListener('click', () => {
// Show the prompt
installPromptEvent.prompt();
hideInstallBtn();
});
Hosting-Möglichkeiten zum Testen eines Deployments:
Service worker sind client-seitige Proxies zwischen Webbrowser und Server.
Service worker können Resourcen cachen und sie entweder aus dem Netzwerk oder dem internen Cache abrufen.
Es läuft zu jeder Domain / registrierten URL genau ein ServiceWorker
Service worker sind besondere Web-Worker, daher:
Serviceworker werden unterstützt ⇒ ES2015 wird unterstützt
Bei Entscheidung für eine Strategie sind verschiedene Ziele in Erwägung zu ziehen:
Aspekte zu Resourcen:
Wichtige Fragen:
laden von Resourcen - Strategien:
Caching - Strategien:
(diese Strategien können kombiniert werden)
Siehe Offline Cookbook
Workbox bietet Unterstützung für verschiedene Service Worker Strategien
Abruf von Resourcen:
NetworkOnly
CacheOnly
NetworkFirst
(Cache, falls Netzwerk nicht verfügbar)CacheFirst
(Netzwerk, falls Eintrag nicht im Cache)StaleWhileRevalidate
(Laden aus dem Cache, welcher im Hintergrund aktualisiert wird)Cache:
precacheAndRoute
fetch
und UntenstehendemNetworkFirst
, CacheFirst
, StaleWhileRevalidate
workbox.routing.registerRoute(
new RegExp('/static/.*'),
new workbox.strategies.CacheFirst()
);
workbox.routing.registerRoute(
'/articles.json',
new workbox.strategies.NetworkFirst()
);
maxEntries
, maxAgeSeconds
)workbox.precaching.precacheAndRoute([
'/',
'/index.html',
'/logo.svg',
]);
Workbox CLI: Werkzeug, um insbesondere Precaching zu vereinfachen
workbox wizard --injectManifest
~45 min
https://codelabs.developers.google.com/codelabs/workbox-lab/
(aktualisiere die Version von workbox-cli
in package.json - ältere Versionen schlagen unter Windows fehl)
Verwandle eine dieser Anwendungen in eine PWA und verwende verschiedene Caching-Strategien:
https://developers.google.com/web/tools/workbox/guides/codelabs/npm-script
Siehe Präsentation Javascript: async and network requests
Möglichkeit, Scripts im Hintergrund (in einem eigenen Thread) laufen zu lassen
Können genutzt werden, um intensive Berechnungen durchzuführen - blockieren die User-Interaktion mit der Website nicht.
const worker = new Worker('worker.js');
worker.onmessage = function(message) {
console.log(message.data);
};
worker.postMessage(42);
onmessage = function(message) {
const result = longComputation();
postMessage(result);
};
Beim hin-und-her-Ãœbergeben von Daten: Daten werden kopiert und als 'plain' JS-Objekte verwertet
Im WebWorker laufen lassen:
function fib(n) {
if (n <= 2) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
= "a request to response map"
Resourcen können von der aktuellen Domain oder auch von fremden Domains gecached werden.
Unterscheidung von drei Arten:
Beispiel: siehe die stock app Beispiele in den Chrome Devtools
Jedes Mal wenn eine Seite geladen wird, rufen wir navigator.serviceWorker.register
mit der URL des Service Workers als Parameter auf. Wenn eine neue oder geänderte Service Worker Datei gefunden wird, wird diese installiert.
window.addEventListener('load', () => {
// registration can be deferred until
// completion of page load
if (navigator.serviceWorker) {
navigator.serviceWorker
.register('/serviceworker.js')
.then(registration => {
// is executed if there is a *new* sw file
console.log(
`SW registered for ${registration.scope}`
);
})
.catch(/* reg failed */);
}
});
Ãœblicherweise behandelt ein Service Worker alle Anfragen, die auf dem Server in seinem "Ordner" liegen.
navigator.serviceWorker.register('/css/serviceworker.js');
Der Service Worker behandelt Anfragen an /css/default.css, aber nicht an /index.html.
Wir können einen Service Worker auch weiter einschränken:
navigator.serviceWorker.register('/css/serviceworker.js', {
scope: '/css/xyz/
})
Das install
Event wird ausgelöst, wenn es eine neue Service Worker Datei gibt:
Guter Zeitpunkt, um Resourcen für die spätere Verwendung herunterzuladen und dem Cache hinzuzufügen
self.addEventListener('install', event => {
console.log(event);
});
Wenn zuvor kein Service Worker vorhanden war, wird der Service Worker sofort nach der Installation aktiv
Wenn zuvor ein anderer Service Worker vorhanden war, wird dieser nach einem "Neustart" der Anwendung aktiv (wenn alle entsprechenden Tabs geschlossen wurden)
Event activate
: gute Gelegenheit, um alte Caches zu bereinigen
self.addEventListener('activate', event => {
console.log(event);
});
Wir können eine sofortige Aktivierung eines neuen Service Workers aus dem install
-Event veranlassen:
self.addEventListener('install', event => {
self.skipWaiting();
});
Deinstallation aller Service Worker für diese Domain:
navigator.serviceWorker
.getRegistrations()
.then(function(registrations) {
for (let registration of registrations) {
registration.unregister();
}
});
// this code can be executed in the
// browser console for any website
const url = '/';
fetch(url)
.then(response => response.text())
.then(console.log);
self.addEventListener('fetch', event => {
event.respondWith(
new Response('All pages look like this')
);
});
Ãœbung: wir erstellen eine kleine lokale Website mit Seiten wie /home, /about, ...
self.addEventListener('fetch', event => {
if (new RegExp('/about/$ ').test(event.request.url)) {
event.respondWith(new Response('About'));
} else if (new RegExp('/a$ ').test(event.request.url)) {
event.respondWith(new Response('Home'));
} else {
event.respondWith(new Response('404'));
}
});
Ãœbung: Loggen aller Netzwerkanfragen, die Netzwerkanfragen dann mittels fetch
beantworkten lassen
self.addEventListener('fetch', event => {
console.log(event);
return fetch(event.request);
});
Wichtige verwandte Technologien:
= "a request to response map"
Durch die globale Variable caches.open
oder self.caches.open
im Service Worker
Promise:
let myCache;
caches.open('test', mc => {
myCache = mc;
});
Cache-Methoden:
myCache.add(request)
myCache.addAll(requests)
myCache.put(request, response)
myCache.delete(request)
myCache.match(request)
myCache.matchAll(requests)
Die Variable request
kann entweder ein String sein, oder ein Request
objekt.
Wir übergeben eine URL; die Resource wird automatisch angefragt und gespeichert
cache.add('/main.js');
cache.addAll(['/', '/index.html', '/main.js']);
Kann verwendet werden, wenn wir schon über die Antwort verfügen
fetch('myurl').then(response => {
console.log(response.clone());
cache.put('myurl', response.clone());
cache.put('otherurl', response);
});
cache.delete('myurl');
Einen Eintrag aus dem Cache holen, der auf einen bestimmten Request passt
// returns a response or undefined
const content = cache.match('myurl');
Eine Anwendung, die Resourcen bei der Installation cacht und sie dauerhaft aus dem Cache zur Verfügung stellt
self.addEventListener('install', () => {
cache.addAll(['/', '/index.html', '/about'])
})
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request);
)
})
self.addEventListener('install', installEvent => {
// wait for the cache to be populated;
// abort install on error
installEvent.waitUntil(
caches.open('app-shell-cache-v3').then(cache => {
return cache.addAll(['/', '/index.html', '/about']);
})
);
// optional - don't abort install on error
caches.open('app-shell-cache-v3').then(cache => {
cache.addAll['/icon1.png'];
});
});
Ein Aufruf von waitUntil
kann verwendet werden, um anzuzeigen, ob die Installation erfolgreich war - dier Service Worker wird nur bei Erfolg aktiviert
alte Einträge löschen:
self.addEventListener('activate', activateEvent => {
activateEvent.waitUntil(
Promise.all([
caches.delete('app-shell-cache-v2'),
caches.delete('app-shell-cache-v1'),
])
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches
.match(event.request)
.then(response => response || fetch(event.request))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
fetch(event.request).then(response => {
cache.put(event.request, response.clone());
return response;
})
);
});
https://developers.google.com/web/ilt/pwa/lab-scripting-the-service-worker
https://developers.google.com/web/fundamentals/instant-and-offline/offline-cookbook/
localStorage ist ein einfacher key-value-Store im Browser; Sowohl keys als auch values sind strings
Der Browser speichert Daten seperat für jede Domain
wichtige Methoden:
localStorage.setItem('name', 'John')
localStorage.getItem('name')
localStorage.removeItem('name')
Speichern und Laden von Daten:
const todoString = JSON.stringify(todos);
localStorage.setItem('todos', todoString);
const todoString = localStorage.getItem('todos');
todos = JSON.parse(todoString);
Vollwertige Datenbank
Vorteile gegenüber localStorage:
Nachteil: Komplexeres Interface
library, die es erlaubt, bei indexedDB mit Promises zu arbeiten
https://github.com/jakearchibald/idb
Einbinden über CDN: https://cdn.jsdelivr.net/npm/idb@2.1.2/lib/idb.min.js
Erstellen / Öffnen einer DB; gibt ein Promise zurück
idb.open(name, version, upgradeCallback);
Beispiel
const upgradeCallback = upgradeDb => {
if (!upgradeDb.objectStoreNames.contains('todos')) {
upgradeDb.createObjectStore('todos', {
autoIncrement: true,
keyPath: 'key',
});
}
};
const dbPromise = idb.open('todo-db', 1, upgradeCallback);
Letztes Argument (upgradeCallback
) kann zur Migration auf ein neues Datenbankschema genutzt werden; z.B. können darin Stores erstellt, gelöscht oder abgeändert werden
Callbackfunktion wird immer aufgerufen, wenn sich die Versionsnummer der Datenbank erhöht
Jedes Element im object store hat einen einzigartigen key (~id);
Der key kann ein Eintrag im Objekt sein oder ein unabhängiger Wert
upgradeDb.createObjectStore('todos', {
autoIncrement: true,
});
upgradeDb.createObjectStore('todos', {
autoIncrement: true,
keyPath: 'key',
});
Verwendung eines Eintrags im Objekt als Key
upgradeDb.createObjectStore('users', {
keyPath: 'email',
});
Transaktion = Gruppe von Operationen auf der Datenbank (auslesen / hinzufügen / überschreiben ...)
let db;
idb.open('todo-db', 1).then(openedDb => {
db = openedDb;
});
const transaction = db.transaction(['todos'], 'readwrite');
const todoStore = transaction.objectStore('todos');
todoStore.add({ text: 'groceries', done: false });
const transaction = db.transaction(['todos'], 'readwrite');
const todoStore = transaction.objectStore('todos');
// ersetze den Eintrag mit index 1
todoStore.put({ text: 'groceris', done: true, key: 1 });
const transaction = db.transaction(['todos'], 'readwrite');
const todoStore = transaction.objectStore('todos');
todoStore.delete(1);
const transaction = db.transaction(['artists'], 'readonly');
const artistsStore = transaction.objectStore('artists');
artistsStore.getAll().then(console.log);
Auslesen anhand des keys:
const transaction = db.transaction(['artists'], 'readonly');
const artistsStore = transaction.objectStore('artists');
artistsStore.get(1).then(console.log);
Die Einträge werden in der Datenbank im wesentlichen nach dem key sortiert abgelegt.
Dadurch kann von der Datenbank schnell nach dem key gesucht werden.
Beispiel: In einem Telefonbuch kann man schnell nach einem Nachnamen suchen, jedoch nicht nach einem Vornamen oder einer Telefonnummer
Um schnell nach etwas anderem als dem primary key zu suchen: zusätzlicher Index (aber: größerer Datenverbrauch)
const store = upgradeDb.createObjectStore('contacts');
store.createIndex('email', 'email', { unique: true });
store.createIndex('firstName', 'firstName');
store.createIndex('name', ['lastName', 'firstName']);
const nameIndex = objectStore.index('name');
nameIndex.get(['Andy', 'Jones']).then(...)
Möglichkeit, für den Benutzer Benachrichtigungen außerhalb der Anwendung darzustellen (Betriebssystems-Benachrichtigungen)
let notificationsAllowed;
Notification.requestPermission().then(result => {
if (result === 'granted') {
notificationsAllowed = true;
}
});
Kann zu jeder Seite in der Browser-Konsole ausprobiert werden
if (Notification.permission === 'granted') {
new Notification('Hello world');
}
new Notification('cloudy', {
body: 'The weather in Vienna is cloudy',
icon: 'static/images/cloudy.png',
vibrate: [100, 50, 100],
});
Die bisherigen Benachrichtigungen stammten aus einem bestimmten Browser-Fenster. Benachrichtigungen können auch aus dem Service Worker dargestellt werden. Diese Benachrichtigungen bieten mehr Möglichkeiten, insbesondere:
let serviceWorkerRegistration = null;
navigator.serviceWorker
.getRegistration()
.then(registration => {
serviceWorkerRegistration = registration;
});
serviceWorkerRegistration.showNotification('cloudy', {
body: 'The weather in Vienna is cloudy',
icon: 'static/images/cloudy.png',
vibrate: [100, 50, 100],
// new option available:
actions: [
{ action: 'close', title: 'Close' },
{ action: 'details', title: 'Details' },
],
});
Zwei Events im ServiceWorker:
notificationclick
notificationclose
https://developers.google.com/web/ilt/pwa/lab-integrating-web-push
1-3
Entfernen der service-worker in FF: about:debugging -> worker
Push-Benachrichtigungen werden über den Browserhersteller (Google, Mozilla, ...) gesendet. Dies geschieht über URLs wie diese:
https://android.googleapis.com/gcm/send/IDENTIFIER
https://updates.push.services.mozilla.com/wpush/v1/IDENTIFIER
https://android.googleapis.com/gcm/send/IDENTIFIER
https://updates.push.services.mozilla.com/wpush/v1/IDENTIFIER
Aktivierung im Browser:
serviceWorkerRegistration.pushManager
.subscribe({
userVisibleOnly: true,
})
.then(subscription => {
console.log(subscription.endpoint);
// could be: https://android.googleapis.com/gcm/send/..
});
Aktuelle Subscription auslesen:
serviceWorkerRegistration.pushManager
.getSubscription()
.then(subsription => {
if (subscription !== undefined) {
console.log(JSON.stringify(subscription.toJSON()));
// send the subscription object to our server
}
});
Sobald wir diese Daten am Server haben, können wir Benachrichtigungen an den Client senden
{
"endpoint": "https://android.googleapis.com/gcm/send/f2L...",
"keys": {
"auth": "5I2BuN...",
"p256dh": "BLc45n..."
}
}
in node.js:
const webPush = require('web-push');
const subscripton = {
endpoint: '...',
keys: { auth: '...', p256dh: '...' },
};
webPush.sendNotification(subscription, 'Hello world!');
In Chrome werden Push-Benachrichtigungen via Firebase Cloud Messaging (früher: Google Cloud Messaging) gesendet
Für die Entwicklung benötigen wir einen Firebase Account und API key
webPush.sendNotification(subscription, 'Hello world!', {
gcmAPIKey: '....',
});
Eine Push-Nachricht muss nicht unbedingt zu einer Benachrichtigung für den Benutzer führen
In Chrome muss aktuell das Empfangen einer Push-Nachricht zu einer Benachrichtigung führen; in Firefox ist die Anzahl der empfangenen Push-Nachrichten ohne Benachrichtigung beschränkt
https://developers.google.com/web/ilt/pwa/lab-integrating-web-push
Publishing PWAs in App Stores
TWA = Trusted Web Activity = Möglichkeit, eine PWA im Play Store zu veröffentlichen
https://developers.google.com/web/updates/2019/02/using-twa
PWAs (bzw HTML-Anwendungen im Allgemeinen) können für veschiedene Stores veröffentlicht werden, selbst wenn diese keine direkte Unterstützung für PWAs bieten: