Ein umfassender Leitfaden für progressive Webanwendungen
Veröffentlicht: 2022-03-10Es war der Geburtstag meines Vaters und ich wollte einen Schokoladenkuchen und ein T-Shirt für ihn bestellen. Ich ging zu Google, um nach Schokoladenkuchen zu suchen, und klickte auf den ersten Link in den Suchergebnissen. Es gab für ein paar Sekunden einen leeren Bildschirm; Ich verstand nicht, was geschah. Nachdem ich ein paar Sekunden geduldig gestarrt hatte, füllte sich mein mobiler Bildschirm mit köstlich aussehenden Kuchen. Sobald ich auf einen von ihnen klickte, um seine Details zu überprüfen, erhielt ich ein hässliches, fettes Popup, in dem ich aufgefordert wurde, eine Android-Anwendung zu installieren, damit ich bei der Bestellung eines Kuchens ein seidenweiches Erlebnis habe.
Das war enttäuschend. Mein Gewissen erlaubte mir nicht, auf den „Installieren“-Button zu klicken. Ich wollte nur noch einen kleinen Kuchen bestellen und mich auf den Weg machen.
Ich klickte auf das Kreuzsymbol ganz rechts im Popup, um es so schnell wie möglich zu verlassen. Aber dann saß das Installations-Popup am unteren Rand des Bildschirms und nahm ein Viertel des Platzes ein. Und mit der flockigen Benutzeroberfläche war das Herunterscrollen eine Herausforderung. Ich habe es irgendwie geschafft, einen holländischen Kuchen zu bestellen.
Nach dieser schrecklichen Erfahrung bestand meine nächste Herausforderung darin, ein Hemd für meinen Vater zu bestellen. Wie zuvor suche ich bei Google nach Hemden. Ich klickte auf den ersten Link und im Handumdrehen lag der gesamte Inhalt direkt vor mir. Das Scrollen war reibungslos. Kein Installationsbanner. Ich hatte das Gefühl, als würde ich eine native Anwendung durchsuchen. Es gab einen Moment, in dem meine schreckliche Internetverbindung aufgab, aber ich konnte immer noch den Inhalt anstelle eines Dinosaurierspiels sehen. Trotz meines nervigen Internets habe ich es geschafft, ein Hemd und eine Jeans für meinen Vater zu bestellen. Am überraschendsten war, dass ich Benachrichtigungen über meine Bestellung erhielt.
Ich würde dies als ein seidenweiches Erlebnis bezeichnen. Diese Leute haben etwas richtig gemacht. Jede Website sollte dies für ihre Benutzer tun. Es heißt progressive Web-App.
Wie Alex Russell in einem seiner Blog-Beiträge feststellt:
„Im Internet kommt es von Zeit zu Zeit vor, dass leistungsstarke Technologien ohne die Vorteile von Marketingabteilungen oder eleganten Verpackungen entstehen. Sie verweilen und wachsen an den Rändern und werden für eine winzige Gruppe zum alten Hut, während sie für alle anderen fast unsichtbar bleiben. Bis jemand ihnen einen Namen gibt.“
Eine seidenweiche Erfahrung im Web, manchmal auch als progressive Webanwendung bekannt
Progressive Webanwendungen (PWAs) sind eher eine Methode, die eine Kombination von Technologien beinhaltet, um leistungsstarke Webanwendungen zu erstellen. Mit einer verbesserten Benutzererfahrung verbringen die Menschen mehr Zeit auf Websites und sehen mehr Werbung. Sie neigen dazu, mehr zu kaufen, und mit Benachrichtigungsaktualisierungen besuchen sie sie häufiger. Die Financial Times hat ihre nativen Apps im Jahr 2011 aufgegeben und eine Web-App mit den damals besten verfügbaren Technologien entwickelt. Mittlerweile ist das Produkt zu einer vollwertigen PWA herangewachsen.
Aber warum sollten Sie nach all dieser Zeit eine Web-App erstellen, wenn eine native App die Arbeit gut genug erledigt?
Sehen wir uns einige der Messwerte an, die in Google IO 17 geteilt werden.
Fünf Milliarden Geräte sind mit dem Internet verbunden, was das Internet zur größten Plattform in der Geschichte der Computer macht. Im mobilen Web besuchen monatlich 11,4 Millionen Einzelbesucher die Top-1000-Webseiten und 4 Millionen die Top-1000-Apps. Das mobile Web verzeichnet etwa viermal so viele Nutzer wie native Anwendungen. Aber diese Zahl sinkt stark, wenn es um Engagement geht.
Ein Nutzer verbringt durchschnittlich 188,6 Minuten in nativen Apps und nur 9,3 Minuten im mobilen Web. Native Anwendungen nutzen die Leistungsfähigkeit von Betriebssystemen, um Push-Benachrichtigungen zu senden, um Benutzern wichtige Updates zukommen zu lassen. Sie bieten eine bessere Benutzererfahrung und booten schneller als Websites in einem Browser. Anstatt eine URL in den Webbrowser einzugeben, müssen Benutzer nur auf das Symbol einer App auf dem Startbildschirm tippen.
Die meisten Besucher des Webs werden wahrscheinlich nicht wiederkommen, also haben sich die Entwickler die Problemumgehung ausgedacht, ihnen Banner zur Installation nativer Anwendungen zu zeigen, um sie intensiv zu beschäftigen. Aber dann müssten Benutzer die lästige Prozedur durchlaufen, die Binärdatei einer nativen Anwendung zu installieren. Benutzer zu zwingen, eine Anwendung zu installieren, ist ärgerlich und verringert die Wahrscheinlichkeit, dass sie sie überhaupt installieren, weiter. Die Chance für das Web ist klar.
Empfohlene Lektüre : Native und PWA: Entscheidungen, keine Herausforderer!
Wenn Webanwendungen mit einer reichhaltigen Benutzererfahrung, Push-Benachrichtigungen, Offline-Unterstützung und sofortigem Laden ausgestattet sind, können sie die Welt erobern. Dies ist, was eine progressive Webanwendung tut.
Eine PWA bietet eine reichhaltige Benutzererfahrung, da sie mehrere Stärken hat:
Schnell
Die Benutzeroberfläche ist nicht flockig. Das Scrollen ist flüssig. Und die App reagiert schnell auf Benutzerinteraktionen.Zuverlässig
Eine normale Website zwingt die Benutzer zu warten und nichts zu tun, während sie damit beschäftigt ist, Fahrten zum Server durchzuführen. Eine PWA hingegen lädt Daten sofort aus dem Cache. Eine PWA funktioniert nahtlos, sogar bei einer 2G-Verbindung. Jede Netzwerkanforderung zum Abrufen eines Assets oder Datenstücks durchläuft einen Service-Worker (dazu später mehr), der zunächst überprüft, ob sich die Antwort für eine bestimmte Anforderung bereits im Cache befindet. Wenn Benutzer fast sofort echte Inhalte erhalten, selbst bei einer schlechten Verbindung, vertrauen sie der App mehr und sehen sie als zuverlässiger an.Engagiert
Eine PWA kann sich einen Platz auf dem Startbildschirm des Benutzers verdienen. Es bietet ein natives App-ähnliches Erlebnis, indem es einen Vollbild-Arbeitsbereich bereitstellt. Es nutzt Push-Benachrichtigungen, um die Benutzer zu beschäftigen.
Nachdem wir nun wissen, was PWAs auf den Tisch bringen, wollen wir uns im Detail damit befassen, was PWAs einen Vorteil gegenüber nativen Anwendungen verschafft. PWAs werden mit Technologien wie Service Workern, Web-App-Manifesten, Push-Benachrichtigungen und IndexedDB/lokaler Datenstruktur für das Caching erstellt. Schauen wir uns jeden im Detail an.
Servicemitarbeiter
Ein Service Worker ist eine JavaScript-Datei, die im Hintergrund ausgeführt wird, ohne die Interaktionen des Benutzers zu beeinträchtigen. Alle GET-Anforderungen an den Server durchlaufen einen Dienstmitarbeiter. Es verhält sich wie ein clientseitiger Proxy. Durch das Abfangen von Netzwerkanfragen übernimmt es die vollständige Kontrolle über die Antwort, die an den Client zurückgesendet wird. Eine PWA wird sofort geladen, da Servicemitarbeiter die Abhängigkeit vom Netzwerk eliminieren, indem sie mit Daten aus dem Cache antworten.
Ein Servicemitarbeiter kann nur eine Netzwerkanfrage abfangen, die in seinem Geltungsbereich liegt. Beispielsweise kann ein Service-Worker im Root-Bereich alle Abrufanforderungen abfangen, die von einer Webseite kommen. Ein Service Worker arbeitet als ereignisgesteuertes System. Es geht in einen Ruhezustand, wenn es nicht benötigt wird, wodurch Speicher gespart wird. Um einen Service Worker in einer Webanwendung zu verwenden, müssen wir ihn zuerst auf der Seite mit JavaScript registrieren.
(function main () { /* navigator is a WEB API that allows scripts to register themselves and carry out their activities. */ if ('serviceWorker' in navigator) { console.log('Service Worker is supported in your browser') /* register method takes in the path of service worker file and returns a promises, which returns the registration object */ navigator.serviceWorker.register('./service-worker.js').then (registration => { console.log('Service Worker is registered!') }) } else { console.log('Service Worker is not supported in your browser') } })()
Wir prüfen zunächst, ob der Browser Servicemitarbeiter unterstützt. Um einen Servicemitarbeiter in einer Webanwendung zu register
, stellen wir seine URL als Parameter für die in navigator.serviceWorker
verfügbare Registrierungsfunktion bereit ( navigator
ist eine Web-API, die es Skripts ermöglicht, sich selbst zu registrieren und ihre Aktivitäten auszuführen). Ein Servicemitarbeiter wird nur einmal registriert. Die Registrierung erfolgt nicht bei jedem Seitenaufruf. Der Browser lädt die Service-Worker-Datei ( ./service-worker.js
) nur herunter, wenn es einen Byte-Unterschied zwischen dem vorhandenen aktivierten Service-Worker und dem neueren gibt oder wenn sich seine URL geändert hat.
Der obige Service-Worker fängt alle Anfragen ab, die vom Stamm ( /
) kommen. Um den Umfang eines Servicemitarbeiters einzuschränken, würden wir einen optionalen Parameter mit einem der Schlüssel als Umfang übergeben.
if ('serviceWorker' in navigator) { /* register method takes in an optional second parameter as an object. To restrict the scope of a service worker, the scope should be provided. scope: '/books' will intercept requests with '/books' in the url. */ navigator.serviceWorker.register('./service-worker.js', { scope: '/books' }).then(registration => { console.log('Service Worker for scope /books is registered', registration) }) }
Der obige Dienstmitarbeiter fängt Anfragen mit /books
in der URL ab. Zum Beispiel wird es Anfragen mit /products
nicht abfangen, aber es könnte sehr wohl Anfragen mit /books/products
abfangen.
Wie bereits erwähnt, arbeitet ein Service Worker als ereignisgesteuertes System. Er lauscht auf Events (installieren, aktivieren, holen, pushen) und ruft entsprechend den jeweiligen Event-Handler auf. Einige dieser Ereignisse sind Teil des Lebenszyklus eines Servicemitarbeiters, der diese Ereignisse nacheinander durchläuft, um aktiviert zu werden.
Installation
Sobald ein Service Worker erfolgreich registriert wurde, wird ein Installationsereignis ausgelöst. Dies ist ein guter Ort, um die Initialisierungsarbeit durchzuführen, z. B. das Einrichten des Caches oder das Erstellen von Objektspeichern in IndexedDB. (IndexedDB wird für Sie sinnvoller, sobald wir uns mit den Details befassen. Im Moment können wir nur sagen, dass es sich um eine Schlüssel-Wert-Paar-Struktur handelt.)
self.addEventListener('install', (event) => { let CACHE_NAME = 'xyz-cache' let urlsToCache = [ '/', '/styles/main.css', '/scripts/bundle.js' ] event.waitUntil( /* open method available on caches, takes in the name of cache as the first parameter. It returns a promise that resolves to the instance of cache All the URLS above can be added to cache using the addAll method. */ caches.open(CACHE_NAME) .then (cache => cache.addAll(urlsToCache)) ) })
Hier cachen wir einige der Dateien, damit der nächste Ladevorgang sofort erfolgt. self
bezieht sich auf die Service-Worker-Instanz. event.waitUntil
lässt den Service Worker warten, bis der gesamte darin enthaltene Code ausgeführt wurde.
Aktivierung
Sobald ein Service Worker installiert wurde, kann er noch nicht auf Abrufanforderungen lauschen. Stattdessen wird ein activate
ausgelöst. Wenn kein aktiver Servicemitarbeiter im gleichen Umfang auf der Website tätig ist, wird der installierte Servicemitarbeiter sofort aktiviert. Wenn eine Website jedoch bereits einen aktiven Servicemitarbeiter hat, wird die Aktivierung eines neuen Servicemitarbeiters verzögert, bis alle Registerkarten geschlossen sind, die auf dem alten Servicemitarbeiter arbeiten. Dies ist sinnvoll, da der alte Service-Worker möglicherweise die Instanz des Caches verwendet, die jetzt im neueren geändert wird. Der Aktivierungsschritt ist also ein guter Ort, um alte Caches loszuwerden.
self.addEventListener('activate', (event) => { let cacheWhitelist = ['products-v2'] // products-v2 is the name of the new cache event.waitUntil( caches.keys().then (cacheNames => { return Promise.all( cacheNames.map( cacheName => { /* Deleting all the caches except the ones that are in cacheWhitelist array */ if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName) } }) ) }) ) })
Im obigen Code löschen wir den alten Cache. Wenn der Name eines Caches nicht mit der cacheWhitelist
, wird er gelöscht. Um die Wartephase zu überspringen und den Service Worker sofort zu aktivieren, verwenden wir skip.waiting()
.
self.addEventListener('activate', (event) => { self.skipWaiting() // The usual stuff })
Sobald der Service Worker aktiviert ist, kann er auf Abrufanforderungen und Push-Ereignisse warten.
Event-Handler abrufen
Immer wenn eine Webseite eine Abrufanforderung für eine Ressource über das Netzwerk auslöst, wird das Abrufereignis vom Dienstmitarbeiter aufgerufen. Der Abrufereignishandler sucht zuerst nach der angeforderten Ressource im Cache. Wenn es im Cache vorhanden ist, gibt es die Antwort mit der zwischengespeicherten Ressource zurück. Andernfalls initiiert er eine Abrufanforderung an den Server, und wenn der Server die Antwort mit der angeforderten Ressource zurücksendet, legt er sie für nachfolgende Anforderungen in den Cache.
/* Fetch event handler for responding to GET requests with the cached assets */ self.addEventListener('fetch', (event) => { event.respondWith( caches.open('products-v2') .then (cache => { /* Checking if the request is already present in the cache. If it is present, sending it directly to the client */ return cache.match(event.request).then (response => { if (response) { console.log('Cache hit! Fetching response from cache', event.request.url) return response } /* If the request is not present in the cache, we fetch it from the server and then put it in cache for subsequent requests. */ fetch(event.request).then (response => { cache.put(event.request, response.clone()) return response }) }) }) ) })
event.respondWith
ermöglicht es dem Servicemitarbeiter, eine angepasste Antwort an den Client zu senden.
Offline-First ist jetzt eine Sache. Für alle nicht kritischen Anfragen müssen wir die Antwort aus dem Cache bereitstellen, anstatt zum Server zu fahren. Wenn ein Asset nicht im Cache vorhanden ist, erhalten wir es vom Server und cachen es dann für nachfolgende Anfragen.
Servicemitarbeiter arbeiten nur auf HTTPS-Websites, da sie die Möglichkeit haben, die Antwort auf Abrufanforderungen zu manipulieren. Jemand mit böswilliger Absicht könnte die Antwort auf eine Anfrage auf einer HTTP-Website manipulieren. Daher ist das Hosten einer PWA auf HTTPS obligatorisch. Servicemitarbeiter unterbrechen den normalen Betrieb des DOM nicht. Sie können nicht direkt mit der Webseite kommunizieren. Um eine Nachricht an eine Webseite zu senden, werden Post-Nachrichten verwendet.
Web-Push-Benachrichtigungen
Angenommen, Sie sind damit beschäftigt, ein Spiel auf Ihrem Handy zu spielen, und eine Benachrichtigung erscheint, die Sie über einen Rabatt von 30 % auf Ihre Lieblingsmarke informiert. Kurzerhand klickst du auf die Benachrichtigung und shoppst dir die Luft aus. Live-Updates beispielsweise zu einem Cricket- oder Fußballspiel zu erhalten oder wichtige E-Mails und Erinnerungen als Benachrichtigungen zu erhalten, ist eine große Sache, wenn es darum geht, Benutzer mit einem Produkt zu begeistern. Diese Funktion war nur in nativen Anwendungen verfügbar, bis PWA auf den Markt kam. Eine PWA nutzt Web-Push-Benachrichtigungen, um mit dieser leistungsstarken Funktion zu konkurrieren, die native Apps standardmäßig bieten. Ein Benutzer würde immer noch eine Web-Push-Benachrichtigung erhalten, selbst wenn die PWA in keinem der Browser-Tabs geöffnet ist und selbst wenn der Browser nicht geöffnet ist.
Eine Webanwendung muss den Benutzer um Erlaubnis bitten, ihm Push-Benachrichtigungen zu senden.

Sobald der Benutzer durch Klicken auf die Schaltfläche „Zulassen“ bestätigt, wird vom Browser ein eindeutiges Abonnement-Token generiert. Dieses Token ist für dieses Gerät eindeutig. Das Format des von Chrome generierten Abonnement-Tokens lautet wie folgt:
{ "endpoint": "https://fcm.googleapis.com/fcm/send/c7Veb8VpyM0:APA91bGnMFx8GIxf__UVy6vJ-n9i728CUJSR1UHBPAKOCE_SrwgyP2N8jL4MBXf8NxIqW6NCCBg01u8c5fcY0kIZvxpDjSBA75sVz64OocQ-DisAWoW7PpTge3SwvQAx5zl_45aAXuvS", "expirationTime": null, "keys": { "p256dh": "BJsj63kz8RPZe8Lv1uu-6VSzT12RjxtWyWCzfa18RZ0-8sc5j80pmSF1YXAj0HnnrkyIimRgLo8ohhkzNA7lX4w", "auth": "TJXqKozSJxcWvtQasEUZpQ" } }
Der im obigen Token enthaltene endpoint
ist für jedes Abonnement eindeutig. Auf einer durchschnittlichen Website würden Tausende von Benutzern zustimmen, Push-Benachrichtigungen zu erhalten, und für jeden von ihnen wäre dieser endpoint
einzigartig. Mit Hilfe dieses endpoint
kann die Anwendung diese Benutzer in Zukunft ansprechen, indem sie ihnen Push-Benachrichtigungen sendet. Die expirationTime
ist die Zeitspanne, die das Abonnement für ein bestimmtes Gerät gültig ist. Wenn die expirationTime
20 Tage beträgt, bedeutet dies, dass das Push-Abonnement des Benutzers nach 20 Tagen abläuft und der Benutzer keine Push-Benachrichtigungen für das ältere Abonnement erhalten kann. In diesem Fall generiert der Browser ein neues Abonnement-Token für dieses Gerät. Zur Verschlüsselung werden die Schlüssel auth
und p256dh
verwendet.
Um in Zukunft Push-Benachrichtigungen an diese Tausende von Benutzern zu senden, müssen wir zunächst ihre jeweiligen Abonnement-Token speichern. Es ist die Aufgabe des Anwendungsservers (des Back-End-Servers, vielleicht ein Node.js-Skript), Push-Benachrichtigungen an diese Benutzer zu senden. Dies mag so einfach klingen, als würde man eine POST
-Anforderung an die Endpunkt-URL mit den Benachrichtigungsdaten in der Anforderungsnutzlast stellen. Es sollte jedoch beachtet werden, dass, wenn ein Benutzer nicht online ist, wenn eine für ihn bestimmte Push-Benachrichtigung vom Server ausgelöst wird, er diese Benachrichtigung trotzdem erhalten sollte, sobald er wieder online ist. Der Server müsste sich um solche Szenarien kümmern und Tausende von Anfragen an die Benutzer senden. Ein Server, der die Verbindung des Benutzers verfolgt, klingt kompliziert. Etwas in der Mitte wäre also für das Routing von Web-Push-Benachrichtigungen vom Server zum Client verantwortlich. Dies wird als Push-Dienst bezeichnet, und jeder Browser hat seine eigene Implementierung eines Push-Dienstes. Der Browser muss dem Push-Dienst folgende Informationen mitteilen, um eine Benachrichtigung zu senden:
- Die Zeit zum Leben
So lange soll eine Nachricht in die Warteschlange gestellt werden, falls sie dem Benutzer nicht zugestellt wird. Nach Ablauf dieser Zeit wird die Nachricht aus der Warteschlange entfernt. - Dringlichkeit der Nachricht
Damit schont der Push-Dienst den Akku des Nutzers, indem er nur Nachrichten mit hoher Priorität sendet.
Der Push-Dienst leitet die Nachrichten an den Client weiter. Da Push vom Client empfangen werden muss, auch wenn seine entsprechende Webanwendung nicht im Browser geöffnet ist, müssen Push-Ereignisse von etwas überwacht werden, das im Hintergrund kontinuierlich überwacht. Sie haben es erraten: Das ist die Aufgabe des Servicemitarbeiters. Der Service Worker lauscht auf Push-Ereignisse und zeigt dem Benutzer Benachrichtigungen an.
Jetzt wissen wir also, dass der Browser, der Push-Service, der Service-Worker und der Anwendungsserver harmonisch zusammenarbeiten, um Push-Benachrichtigungen an den Benutzer zu senden. Schauen wir uns die Implementierungsdetails an.
Web-Push-Client
Das Einholen der Erlaubnis des Benutzers ist eine einmalige Sache. Wenn ein Benutzer bereits die Erlaubnis erteilt hat, Push-Benachrichtigungen zu erhalten, sollten wir nicht erneut nachfragen. Der Berechtigungswert wird in Notification.permission
gespeichert.

/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
/* Notification.permission can have one of these three values: default, granted or denied. */ if (Notification.permission === 'default') { /* The Notification.requestPermission() method shows a notification permission prompt to the user. It returns a promise that resolves to the value of permission*/ Notification.requestPermission().then (result => { if (result === 'denied') { console.log('Permission denied') return } if (result === 'granted') { console.log('Permission granted') /* This means the user has clicked the Allow button. We're to get the subscription token generated by the browser and store it in our database. The subscription token can be fetched using the getSubscription method available on pushManager of the serviceWorkerRegistration object. If subscription is not available, we subscribe using the subscribe method available on pushManager. The subscribe method takes in an object. */ serviceWorkerRegistration.pushManager.getSubscription() .then (subscription => { if (!subscription) { const applicationServerKey = ' ' serviceWorkerRegistration.pushManager.subscribe({ userVisibleOnly: true, // All push notifications from server should be displayed to the user applicationServerKey // VAPID Public key }) } else { saveSubscriptionInDB(subscription, userId) // A method to save subscription token in the database } }) } }) }
In der obigen Methode „ subscribe
“ übergeben wir userVisibleOnly
und „ applicationServerKey
“, um ein Abonnementtoken zu generieren. Die userVisibleOnly
Eigenschaft sollte immer wahr sein, da sie dem Browser mitteilt, dass alle vom Server gesendeten Push-Benachrichtigungen dem Client angezeigt werden. Betrachten wir ein Szenario, um den Zweck von applicationServerKey
zu verstehen.
Wenn jemand Ihre Tausende von Abonnement-Token in die Finger bekommt, könnte er sehr gut Benachrichtigungen an die Endpunkte senden, die in diesen Abonnements enthalten sind. Der Endpunkt kann nicht mit Ihrer eindeutigen Identität verknüpft werden. Um den in Ihrer Webanwendung generierten Abonnement-Token eine eindeutige Identität zuzuweisen, verwenden wir das VAPID-Protokoll. Mit VAPID identifiziert sich der Applikationsserver beim Versenden von Push-Benachrichtigungen freiwillig gegenüber dem Push-Service. Wir generieren zwei Schlüssel wie folgt:
const webpush = require('web-push') const vapidKeys = webpush.generateVAPIDKeys()
web-push ist ein npm-Modul. vapidKeys
wird einen öffentlichen Schlüssel und einen privaten Schlüssel haben. Der oben verwendete Schlüssel des Anwendungsservers ist der öffentliche Schlüssel.
Web-Push-Server
Die Aufgabe des Web-Push-Servers (Anwendungsserver) ist unkompliziert. Es sendet eine Benachrichtigungsnutzlast an die Abonnementtoken.
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
const options = { TTL: 24*60*60, //TTL is the time to live, the time that the notification will be queued in the push service vapidDetails: { subject: '[email protected]', publicKey: ' ', privateKey: ' ' } } const data = { title: 'Update', body: 'Notification sent by the server' } webpush.sendNotification(subscription, data, options)
Es verwendet die sendNotification
Methode aus der Web-Push-Bibliothek.
Servicemitarbeiter
Der Servicemitarbeiter zeigt dem Benutzer die Benachrichtigung so an:
self.addEventListener('push', (event) => { let options = { body: event.data.body, icon: 'images/example.png', } event.waitUntil( /* The showNotification method is available on the registration object of the service worker. The first parameter to showNotification method is the title of notification, and the second parameter is an object */ self.registration.showNotification(event.data.title, options) ) })
Bis jetzt haben wir gesehen, wie ein Servicemitarbeiter den Cache nutzt, um Anfragen zu speichern und eine PWA schnell und zuverlässig zu machen, und wir haben gesehen, wie Web-Push-Benachrichtigungen die Benutzer beschäftigen.
Um eine Reihe von Daten auf der Clientseite für den Offline-Support zu speichern, benötigen wir eine riesige Datenstruktur. Schauen wir uns die PWA der Financial Times an. Sie müssen sich selbst von der Leistungsfähigkeit dieser Datenstruktur überzeugen. Laden Sie die URL in Ihren Browser und schalten Sie dann Ihre Internetverbindung aus. Die Seite erneut laden. Gäh! Funktioniert es noch? Es ist. (Wie ich schon sagte, offline ist das neue Schwarz.) Es kommen keine Daten aus den Kabeln. Es wird vom Haus aus serviert. Gehen Sie zur Registerkarte „Anwendungen“ der Chrome-Entwicklertools. Unter „Storage“ finden Sie „IndexedDB“.

Sehen Sie sich den Objektspeicher „Artikel“ an und erweitern Sie alle Artikel, um sich selbst von der Magie zu überzeugen. Die Financial Times hat diese Daten für den Offline-Support gespeichert. Diese Datenstruktur, die es uns ermöglicht, eine riesige Datenmenge zu speichern, wird IndexedDB genannt. IndexedDB ist eine JavaScript-basierte objektorientierte Datenbank zum Speichern strukturierter Daten. Wir können in dieser Datenbank verschiedene Objektspeicher für verschiedene Zwecke erstellen. Wie wir beispielsweise im obigen Bild sehen können, werden „Resources“, „ArticleImages“ und „Articles“ als Objektspeicher bezeichnet. Jeder Datensatz in einem Objektspeicher wird eindeutig mit einem Schlüssel identifiziert. IndexedDB kann sogar zum Speichern von Dateien und Blobs verwendet werden.
Versuchen wir, IndexedDB zu verstehen, indem wir eine Datenbank zum Speichern von Büchern erstellen.
let openIdbRequest = window.indexedDB.open('booksdb', 1)
Wenn die Datenbank booksdb
noch nicht existiert, erstellt der obige Code eine booksdb
Datenbank. Der zweite Parameter der open-Methode ist die Version der Datenbank. Durch die Angabe einer Version werden schemabezogene Änderungen berücksichtigt, die in Zukunft auftreten können. Zum Beispiel hat booksdb
jetzt nur eine Tabelle, aber wenn die Anwendung wächst, beabsichtigen wir, ihr zwei weitere Tabellen hinzuzufügen. Um sicherzustellen, dass unsere Datenbank mit dem aktualisierten Schema synchron ist, geben wir eine höhere Version als die vorherige an.
Beim Aufrufen der open
-Methode wird die Datenbank nicht sofort geöffnet. Es ist eine asynchrone Anforderung, die ein IDBOpenDBRequest
-Objekt zurückgibt. Dieses Objekt hat Erfolgs- und Fehlereigenschaften; Wir müssen geeignete Handler für diese Eigenschaften schreiben, um den Status unserer Verbindung zu verwalten.
let dbInstance openIdbRequest.onsuccess = (event) => { dbInstance = event.target.result console.log('booksdb is opened successfully') } openIdbRequest.onerror = (event) => { console.log('There was an error in opening booksdb database') } openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result let objectstore = db.createObjectStore('books', { keyPath: 'id' }) }
Um die Erstellung oder Änderung von Objektspeichern zu verwalten (Objektspeicher sind analog zu SQL-basierten Tabellen – sie haben eine Schlüsselwertstruktur), wird die onupgradeneeded
-Methode für das openIdbRequest
Objekt aufgerufen. Die Methode onupgradeneeded
wird immer dann aufgerufen, wenn sich die Version ändert. Im obigen Code-Snippet erstellen wir einen Bücher-Objektspeicher mit einem eindeutigen Schlüssel als ID.
Nehmen wir an, dass wir nach der Bereitstellung dieses Codeabschnitts einen weiteren Objektspeicher mit dem Namen „ users
“ erstellen müssen. Also, jetzt ist die Version unserer Datenbank 2
.
let openIdbRequest = window.indexedDB.open('booksdb', 2) // New Version - 2 /* Success and error event handlers remain the same. The onupgradeneeded method gets called when the version of the database changes. */ openIdbRequest.onupgradeneeded = (event) => { let db = event.target.result if (!db.objectStoreNames.contains('books')) { let objectstore = db.createObjectStore('books', { keyPath: 'id' }) } let oldVersion = event.oldVersion let newVersion = event.newVersion /* The users tables should be added for version 2. If the existing version is 1, it will be upgraded to 2, and the users object store will be created. */ if (oldVersion === 1) { db.createObjectStore('users', { keyPath: 'id' }) } }
Wir haben dbInstance
im Erfolgsereignishandler der offenen Anforderung zwischengespeichert. Um Daten in IndexedDB abzurufen oder hinzuzufügen, verwenden wir dbInstance
. Lassen Sie uns einige Buchdatensätze in unserem Buchobjektspeicher hinzufügen.
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') let bookRecord = { id: '1', name: 'The Alchemist', author: 'Paulo Coelho' } let addBookRequest = objectstore.add(bookRecord) addBookRequest.onsuccess = (event) => { console.log('Book record added successfully') } addBookRequest.onerror = (event) => { console.log('There was an error in adding book record') }
Wir verwenden transactions
, insbesondere beim Schreiben von Datensätzen in Objektspeichern. Eine Transaktion ist einfach ein Wrapper um eine Operation, um die Datenintegrität sicherzustellen. Wenn eine der Aktionen in einer Transaktion fehlschlägt, wird keine Aktion in der Datenbank ausgeführt.
Ändern wir einen Buchdatensatz mit der put
-Methode:
let modifyBookRequest = objectstore.put(bookRecord) // put method takes in an object as the parameter modifyBookRequest.onsuccess = (event) => { console.log('Book record updated successfully') }
Lassen Sie uns einen Bucheintrag mit der get
-Methode abrufen:
let transaction = dbInstance.transaction('books') let objectstore = transaction.objectstore('books') /* get method takes in the id of the record */ let getBookRequest = objectstore.get(1) getBookRequest.onsuccess = (event) => { /* event.target.result contains the matched record */ console.log('Book record', event.target.result) } getBookRequest.onerror = (event) => { console.log('Error while retrieving the book record.') }
Hinzufügen eines Symbols auf dem Startbildschirm
Da es kaum noch einen Unterschied zwischen einer PWA und einer nativen Anwendung gibt, ist es sinnvoll, der PWA eine Spitzenposition einzuräumen. Wenn Ihre Website die grundlegenden Kriterien einer PWA erfüllt (auf HTTPS gehostet, in Servicemitarbeiter integriert und über eine manifest.json
verfügt) und nachdem der Benutzer einige Zeit auf der Webseite verbracht hat, ruft der Browser unten eine Eingabeaufforderung auf und fragt Der Benutzer kann die App zu seinem Startbildschirm hinzufügen, wie unten gezeigt:

Wenn ein Benutzer auf „FT zum Startbildschirm hinzufügen“ klickt, kann die PWA sowohl auf dem Startbildschirm als auch in der App-Schublade ihren Fuß setzen. Wenn ein Benutzer auf seinem Telefon nach einer Anwendung sucht, werden alle PWAs aufgelistet, die der Suchanfrage entsprechen. Sie werden auch in den Systemeinstellungen angezeigt, was es Benutzern erleichtert, sie zu verwalten. In diesem Sinne verhält sich eine PWA wie eine native Anwendung.
PWAs verwenden manifest.json
, um diese Funktion bereitzustellen. Schauen wir uns eine einfache manifest.json
-Datei an.
{ "name": "Demo PWA", "short_name": "Demo", "start_url": "/?standalone", "background_color": "#9F0C3F", "theme_color": "#fff1e0", "display": "standalone", "icons": [{ "src": "/lib/img/icons/xxhdpi.png?v2", "sizes": "192x192" }] }
Der short_name
erscheint auf dem Startbildschirm des Benutzers und in den Systemeinstellungen. Der name
wird in der Chrome-Eingabeaufforderung und auf dem Begrüßungsbildschirm angezeigt. Der Begrüßungsbildschirm ist das, was der Benutzer sieht, wenn die App startbereit ist. Die start_url
ist der Hauptbildschirm Ihrer App. Es ist das, was Benutzer erhalten, wenn sie auf dem Startbildschirm auf ein Symbol tippen. Die background_color
wird auf dem Begrüßungsbildschirm verwendet. Die theme_color
legt die Farbe der Symbolleiste fest. Der standalone
-Wert für den display
besagt, dass die App im Vollbildmodus (Ausblenden der Symbolleiste des Browsers) betrieben werden soll. Wenn ein Benutzer eine PWA installiert, wird ihre Größe lediglich in Kilobyte und nicht in Megabyte nativer Anwendungen angegeben.
Servicemitarbeiter, Web-Push-Benachrichtigungen, IndexedDB und die Position des Startbildschirms machen Offline-Support, Zuverlässigkeit und Engagement wett. Es sollte beachtet werden, dass ein Servicemitarbeiter nicht mit der allerersten Ladung zum Leben erwacht und mit seiner Arbeit beginnt. Der erste Ladevorgang wird immer noch langsam sein, bis alle statischen Assets und anderen Ressourcen zwischengespeichert wurden. Wir können einige Strategien implementieren, um die erste Ladung zu optimieren.
Bündelung von Vermögenswerten
Alle Ressourcen, einschließlich HTML, Stylesheets, Bilder und JavaScript, müssen vom Server abgerufen werden. Je mehr Dateien, desto mehr HTTPS-Anforderungen sind erforderlich, um sie abzurufen. Wir können Bundler wie WebPack verwenden, um unsere statischen Assets zu bündeln, wodurch die Anzahl der HTTP-Anfragen an den Server reduziert wird. WebPack leistet hervorragende Arbeit bei der weiteren Optimierung des Bundles durch den Einsatz von Techniken wie Code-Splitting (d. h. Bündeln nur der Dateien, die für das aktuelle Laden der Seite erforderlich sind, anstatt sie alle zusammen zu bündeln) und Tree Shaking (d. h. Entfernen doppelter Abhängigkeiten oder Abhängigkeiten, die importiert, aber nicht im Code verwendet werden).
Reduzierung von Hin- und Rückfahrten
Einer der Hauptgründe für Langsamkeit im Internet ist die Netzwerklatenz. Die Zeit, die ein Byte benötigt, um von A nach B zu gelangen, variiert mit der Netzwerkverbindung. Beispielsweise dauert ein bestimmter Roundtrip über Wi-Fi 50 Millisekunden und 500 Millisekunden bei einer 3G-Verbindung, aber 2500 Millisekunden bei einer 2G-Verbindung. Diese Anfragen werden über das HTTP-Protokoll gesendet, was bedeutet, dass eine bestimmte Verbindung, während sie für eine Anfrage verwendet wird, nicht für andere Anfragen verwendet werden kann, bis die Antwort der vorherigen Anfrage bedient wird. Eine Website kann gleichzeitig sechs asynchrone HTTP-Anforderungen stellen, da einer Website sechs Verbindungen für HTTP-Anforderungen zur Verfügung stehen. Eine durchschnittliche Website macht ungefähr 100 Anfragen; Bei maximal sechs verfügbaren Verbindungen kann ein Benutzer also am Ende etwa 833 Millisekunden in einem einzigen Roundtrip verbringen. (Die Berechnung ist 833 Millisekunden - 100 ⁄ 6 = 1666 . Wir müssen 1666 durch 2 dividieren, weil wir die für einen Roundtrip aufgewendete Zeit berechnen.) Mit HTTP2 wird die Turnaround-Zeit drastisch reduziert. HTTP2 blockiert den Verbindungskopf nicht, sodass mehrere Anfragen gleichzeitig gesendet werden können.
Die meisten HTTP-Antworten enthalten last-modified
und etag
-Header. Der last-modified
Header ist das Datum, an dem die Datei zuletzt geändert wurde, und ein etag
ist ein eindeutiger Wert, der auf dem Inhalt der Datei basiert. Es wird nur geändert, wenn der Inhalt einer Datei geändert wird. Diese beiden Header können verwendet werden, um das erneute Herunterladen der Datei zu vermeiden, wenn bereits eine zwischengespeicherte Version lokal verfügbar ist. Wenn der Browser eine Version dieser Datei lokal verfügbar hat, kann er einen dieser beiden Header als solchen in die Anfrage einfügen:

Der Server kann überprüfen, ob sich der Inhalt der Datei geändert hat. If the contents of the file have not changed, then it responds with a status code of 304 ( not modified ).

This indicates to the browser to use the locally available cached version of the file. By doing all of this, we've prevented the file from being downloaded.
Faster responses are in now place, but our job is not done yet. We still have to parse the HTML, load the style sheets and make the web page interactive. It makes sense to show some empty boxes with a loader to the user, instead of a blank screen. While the HTML document is getting parsed, when it comes across <script src='asset.js'></script>
, it will make a synchronous HTTP request to the server to fetch asset.js
, and the whole parsing process will be paused until the response comes back. Imagine having a dozen of synchronous static asset references. These could very well be managed just by making use of the async
keyword in script references, like <script src='asset.js' async></script>
. With the introduction of the async
keyword here, the browser will make an asynchronous request to fetch asset.js
without hindering the parsing of the HTML. If a script file is required at a later stage, we can defer the downloading of that file until the entire HTML has been parsed. A script file can be deferred by using the defer
keyword, like <script src='asset.js' defer></script>
.
Fazit
We've learned a lot of many new things that make for a cool web application. Here's a summary of all of the things we've explored in this article:
- Service workers make good use of the cache to speed up the loading of assets.
- Web push notifications work under the hood.
- We use IndexedDB to store a massive amount of data.
- Some of the optimizations for instant first load, like using HTTP2 and adding headers like
Etag
,last-modified
andIf-None-Match
, prevent the downloading of valid cached assets.
That's all, folks!