Webkrauts Logo

Webkrauts Webkrauts Schriftzug

- für mehr Qualität im Web

»Sie haben Post!« – Teil 2

Web Notifications für Desktop und Mobile

»Sie haben Post!« – Teil 2

Nachdem der erste Teil die grundsätzliche Funktion von Web Notifications gezeigt hat, folgt heute ein etwas komplexeres Beispiel. Darin enthalten ist auch eine zeitversetzte Push-Benachrichtigung.

Gestern zeigte das einfache Beispiel, wie eine Notification aussieht und wie ihr sie mit JavaScript schnell erzeugt. Um die Macht dieser Funktion genauer zu demonstrieren, kommen wir jetzt zu einem etwas komplexeren Beispiel, das auch die Serverinfrastruktur verwendet. Da wir in diesem Fall Service Worker zur Demonstration verwenden, läuft diese Demo zum jetzigen Zeitpunkt (Dezember 2015) nur in Chrome, darin allerdings auch mobil auf Android. Zusätzlich erfordern Service Worker zwingend die Verwendung einer verschlüsselten Verbindung. Dies ist aus Sicherheitsgründe so, um Beispielsweise Man-in-the-middle-Attacken zu verhindern. Daher ist die Demo mit einem SSL-Zertifikat ausgestattet.

Das volle Potential spielen Benachrichtigungen erst aus, wenn dahinter eine Nutzer- bzw. eine Aboverwaltung läuft und die Informationen zeitversetzt übermittelbar sind. Damit lassen sich einzelnen Benutzern gezielt Nachrichten senden, um beispielsweise über neue Zustände in einer Web-App zu informieren. Zusammen mit der Push API erhalten wir so die Möglichkeit, Push-Nachrichten an Webappnutzer zu senden – ein Privileg, das bisher nativen Applikationen vorbehalten war.

Um dies in Chrome zu ermöglichen, benötigt unsere Seite zunächst ein Manifest, das der ein oder andere Webworker sicherlich schon von der Verwendung für Web-Apps auf Android kennt.

  1. {  
  2. "name": "Webkrauts Notification Demo für AK 2015",  
  3. "short_name": "Notification Demo",  
  4. "icons": [{  
  5.   "src": "http://webkrauts.de/sites/all/themes/webkrauts/img/webkrauts_bildmarke_small.png",  
  6.   "sizes": "40x40",
  7.   "type": "image/png"
  8. }],  
  9. "start_url": "/index.html",  
  10. "display": "standalone",  
  11. "gcm_sender_id": "HIER GEHÖRT EURE PROJEKTNUMMER HIN"
  12. }

Zusätzlich zu den bekannten Angaben zu Namen, Icon und Verwendung ist lediglich der Hinweis gcm_sender_id hinzugekommen. Dieser ermöglicht die Senderidentifikation. In diesem Beispiel haben wir dazu die Google API verwendet. Um die Demo nachzubauen, legt ihr in der Google Developer Konsole ein neues Projekt an. Dies geht in der Auswahlbox oben links neben dem Logo. Die Projektnummer ist die gewünschte Senderidentifikationsnummer. Sie findet sich auf der Startseite des Projekts im Kasten oben links. Wir werden später noch einen passenden API-Schlüssel generieren.

Ein Screenshot der Developer-Konsole von Google.
In der Google Developer Konsole wird das Projekt für den Versand der Push-Nachrichten angelegt.

Um unsere Push-Nachrichten zu empfangen, registrieren wir jetzt einen Service Worker, soweit der Browser diese Funktion unterstützt. Damit wir sehen, ob soweit alles geklappt hat, legen wir Erfolgs- und Fehlermeldungen in das Log ab. Dieses können wir uns in den Entwicklertools von Chrome anzeigen lassen. Im Erfolgsfall führen wir zusätzlich die Funktion initialiseState aus, mit der wir die Innitalisierung durchführen. Den so registrierten Service Worker schauen wir uns später genauer an.

Außerdem statten wir unseren Abo-Button mit Funktionen aus, mit denen der Nutzer Push-Nachrichten abonnieren (subscribe) oder wieder abbestellen (unsubscribe) kann. Dies dient hier nur der Demonstration des Funktionsablaufs. In einem wirklichen Einsatzszenario müssten wir darüber die Identifikationsnummern der einzelnen Browser sammeln und diese beispielsweise Benutzerkonten in einer Datenbank zuweisen. So ließe sich später gezielt Benutzer X eine Nachricht an all seine autorisierten Geräte senden.

  1. // Variable die den Abozustand referenziert
  2. var isPushEnabled = false;
  3.  
  4. window.addEventListener('load', function() {  
  5.  
  6.   //Registrieren des Service Workers
  7.   if ('serviceWorker' in navigator) {
  8.     navigator.serviceWorker.register('serviceworker.js').then(function(registration) {
  9.  
  10.       // Erfolg
  11.       console.log('ServiceWorker ist erfolgreich registriert mit Bereich: ', registration.scope);
  12.  
  13.       //Status innitalisieren
  14.       initialiseState();
  15.     }).catch(function(err) {
  16.  
  17.       // Fehler
  18.       console.log('ServiceWorker ist NICHT registriert: ', err);
  19.     });
  20.   }
  21.  
  22.   // Den Abo-Button mit Klickfunktion ausstatten
  23.   var pushButton = document.querySelector('.js-push-button');  
  24.   pushButton.addEventListener('click', function() {  
  25.     if (isPushEnabled) {  
  26.       unsubscribe();  
  27.     } else {  
  28.       subscribe();  
  29.     }  
  30.   });
  31.  
  32. });

Schauen wir uns jetzt die Funktion initialiseState an. Zunächst prüfen wir zur Vorsicht darin noch einmal, ob alle benötigten Funktionen und Berechtigungen vorliegen. Das betrifft sowohl die Funktionen des Service Worker als auch die Push API und die Berechtigung für die Notifications.

Anschließend prüfen wir, ob der Service Worker einsetzbar ist und ob der Client – also der Browser – beim Push Manager des Service Worker schon als Abonnent vorliegt. Falls dem Service Worker kein Abo vorliegt, brechen wir ab. Sollte schon ein Abo vorliegen, z.B. bei erneutem Besuch der Seite, bieten wir gleich den Button für den Versand der Nachricht an. In dem Fall ändern wir auch die Beschriftung des Abo-Buttons und ändern die Variable, die den Abozustand enthält.

  1. // Wenn der Service Worker fertig ist, wird initalisiert.
  2. function initialiseState() {  
  3.  
  4.   // Nochmal prüfen, ob alle Funktionen verfügbar sind
  5.   // Auf Notifications prüfen
  6.   if (!('showNotification' in ServiceWorkerRegistration.prototype)) {  
  7.     console.warn('Notifications über Service Worker werden von diesem Browser nicht unterstützt.');  
  8.     return;  
  9.   }
  10.  
  11.   // Prüfen, ob Notifications erlaubt sind
  12.   if (Notification.permission === 'denied') {  
  13.     console.warn('Du hast Notifications blockiert. Entferne diese Einstellung in den Browsersettings.');  
  14.     return;  
  15.   }
  16.  
  17.   // Prüfen, ob die Push-Architektur unterstützt wird
  18.   if (!('PushManager' in window)) {  
  19.     console.warn('Push Nachrichten werden von diesem Browser nicht unterstützt.');  
  20.     return;  
  21.   }
  22.  
  23.   // Ist der Service Worker ready? Dann weiter.
  24.   navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {  
  25.  
  26.     // Gibt es vielleicht schon ein Abo?
  27.     serviceWorkerRegistration.pushManager.getSubscription()  
  28.       .then(function(subscription) {  
  29.  
  30.         // Button für die Abos aktivieren
  31.         var pushButton = document.querySelector('.js-push-button');  
  32.         pushButton.disabled = false;
  33.  
  34.         if (!subscription) {  
  35.           // Es besteht kein Abo, deshalb abbrechen
  36.           return;  
  37.         }
  38.  
  39.         // Es besteht schon ein Abo. Wir können den Sende-Button aktivieren.
  40.         $('#complex-button').show().click(function(e){  
  41.           sendNotification(subscription.endpoint);
  42.         });
  43.  
  44.         // Button ändern
  45.         pushButton.textContent = 'Notifications abbestellen';  
  46.         isPushEnabled = true;  
  47.       })  
  48.       .catch(function(err) {  
  49.         // Es gab einen Fehler bei der Aboprüfung
  50.         console.warn('Der Abozustand konnte nicht geprüft werden.', err);  
  51.       });  
  52.   });  
  53. }

Nun werfen wir einen Blick auf die Funktionen, die das Abo registrieren bzw. wieder abmelden. Dabei wird zunächst der Button blockiert und anschließend das Abo im Push Manager des Service Workers hinzugefügt. Wichtig ist dabei, die Option userVisibleOnly auf true zu setzen, da derzeit nur Notifications unterstützt werden, die für den Benutzer sichtbar sind.

Hat soweit alles geklappt verändern wir den Abobutton und blenden den zusätzlichen Button für den Versand unserer Push-Nachricht ein. Außerdem haben wir wieder ein paar Fehlermeldungen definiert, falls doch etwas schief gehen sollte. Analog verhält sich die Funktion für die Abmeldung des Abos, nur eben andersherum.

Wie zuvor erwähnt müssten wir in beiden Funktionen eigentlich auch eine Benutzerdatenbank pflegen, um später die Clientidentifikation zu ermöglichen und gezielt Inhalte übermitteln zu können. Für diese Demonstration ist dies nicht integriert, aber an der richtigen Stelle durch einen Kommentar angedeutet.

  1. //Funktion zum Abonnieren der Notifications
  2. function subscribe() {  
  3.  
  4.   // Button ausschalten während der Prozess durchläuft
  5.   var pushButton = document.querySelector('.js-push-button');  
  6.   pushButton.disabled = true;
  7.  
  8.   // Die eigentliche Anmeldung im ServiceWorker
  9.   navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {  
  10.     serviceWorkerRegistration.pushManager.subscribe({userVisibleOnly: true})  
  11.       .then(function(subscription) {  
  12.  
  13.         // Die Anmeldung war erfolgreich
  14.         isPushEnabled = true;  
  15.         // Button ändern
  16.         pushButton.textContent = 'Notifications abbestellen';  
  17.         pushButton.disabled = false;
  18.  
  19.         // Hier müsste man eigentlich die eigene Serverarchitektur bedienen,
  20.         // um die Abos später verwalten zu können, z.B. mit einer eigenen Funktion
  21.         // var subscriptionId = pushSubscription.subscriptionId;
  22.         // subscribeToMyService(subscriptionId);
  23.  
  24.         // Wir zeigen jetzt den Sende-Button und fügen das Klick-Event hinzu
  25.         $('#complex-button').show().click(function(e){  
  26.         sendNotification(subscription.endpoint);
  27.         });
  28.  
  29.       })  
  30.       .catch(function(e) {  
  31.         // Falls es Fehler gab, zeigen wir diese an
  32.         if (Notification.permission === 'denied') {  
  33.           // Notifications sind geblockt.
  34.           console.warn('Die Browsereinstellungen verbieten Notifications. Bitte manuell ändern.');  
  35.           pushButton.disabled = true;  
  36.         } else {  
  37.           // Es gibt ein anderes Problem. Reset zur Ausgangslage.
  38.           console.error('Es ist ein unbekanntes Problem aufgetreten.', e);  // Jede App sollte diese Fehlermeldung haben.
  39.           pushButton.disabled = false;  
  40.           pushButton.textContent = 'Notifications aktivieren';  
  41.         }  
  42.       });  
  43.   });  
  44. }
  45.  
  46.  
  47. // Funktion zum Abbestellen der Notifications
  48. function unsubscribe() {  
  49.  
  50.   // Button ausschalten während der Prozess durchläuft
  51.   var pushButton = document.querySelector('.js-push-button');  
  52.   pushButton.disabled = true;
  53.  
  54.   // Die eigentliche Abmeldung im Service Worker
  55.   navigator.serviceWorker.ready.then(function(serviceWorkerRegistration) {  
  56.     serviceWorkerRegistration.pushManager.getSubscription().then(  
  57.       function(pushSubscription) {  
  58.         // Prüfen ob es überhaupt ein Abo gibt
  59.         if (!pushSubscription) {  
  60.           // Falls nicht, reset des Zustands
  61.           isPushEnabled = false;  
  62.           pushButton.disabled = false;  
  63.           pushButton.textContent = 'Notifications abonnieren';  
  64.           return;  
  65.         }  
  66.  
  67.         // Hier müsste man eigentlich die eigene Serverarchitektur bedienen,
  68.         // um die Abos wieder entfernen zu können, z.B. mit einer eigenen Funktion
  69.         // var subscriptionId = pushSubscription.subscriptionId;
  70.         // unsubscribeFromMyService(subscriptionId);  
  71.  
  72.         // Sende-Button verstecken
  73.         $('#complex-button').hide();
  74.  
  75.         // Das Abo aus dem Service Worker entfernen
  76.         pushSubscription.unsubscribe().then(function(successful) {  
  77.           // Button zurücksetzen
  78.           pushButton.disabled = false;  
  79.           pushButton.textContent = 'Notifications abonnieren';  
  80.           isPushEnabled = false;  
  81.         }).catch(function(e) {  
  82.           // Es sind Fehler beim Abbestellen im Service Worker aufgetreten
  83.           console.log('Es ist ein Fehler beim Abbestellen aufgetreten: ', e);  
  84.           pushButton.disabled = false;
  85.           pushButton.textContent = 'Notifications abonnieren';
  86.         });  
  87.       }).catch(function(e) {  
  88.         console.error('Der Abozustand konnte nicht abgefragt werden.', e);  
  89.       });  
  90.   });  
  91. }

Befassen wir uns an dieser Stelle mit dem Service Worker, den wir weiter oben registriert haben. Dieser besteht aus einer JavaScript-Datei, in der wir bestimmte Events registrieren. Hier geht es in erster Linie um das Push-Event, das auf das Eintreffen einer neuen Nachricht reagiert. Es fällt bei dem Codebeispiel sofort auf, dass wir hier eine feste Nachricht vorgeben. Das ist dem Umstand geschuldet, dass derzeit keine Daten über diesen Weg übermittelt werden können. Dies ist zwar für die Zukunft bereits vorgesehen, dafür müssten die Daten allerdings verschlüsselt werden und die dafür benötigten Funktionen sind noch nicht in den Browsern implementiert bzw. es sind noch keine Standards dafür definiert. Um unter diesen Umständen also eine individuelle Nachricht darstellen zu können, müssten wir an dieser Stelle mit einem AJAX-Aufruf auf den Server zugreifen und anhand der Clientidentifikation die hier festgelegten Nachrichteninhalte durch individuelle Elemente ersetzen. Hier käme dazu die oben erwähnte Benutzerdatenbank ins Spiel. Für unsere Demo reicht es, die feste Nachricht anzuzeigen.

Zusätzlich fügen wir auch ein Event für den Klick auf die Meldung ein. Dabei müssen wir für Android die Nachricht auch explizit schließen, da diese sonst offen bliebe. Für ein besseres Nutzererlebnis prüfen wir, ob die Seite vielleicht schon offen ist, und setzen dann den Fokus auf diesen Tab. Ansonsten wird die Seite einfach in einem neuen Tab geöffnet.

  1. // Push-Event registrieren
  2. self.addEventListener('push', function(event) {  
  3.   // Kleiner Hinweis in der Konsole
  4.   console.log('Received a push message', event);
  5.  
  6.   // Der Inhalt der Nachricht
  7.   // In einem richtigen Einsatz müsste man natürlich hier serverseitig Informationen abholen.
  8.   // Daher auch die Notwendigkeit, auf dem Server Informationen zum Abo zu speichern.
  9.   var title = 'Webkrauts AK 2015 Notification';  
  10.   var body = 'Wir wünschen eine besinnliche Weihnachtszeit und frohe Festtage.';
  11.   var icon = 'http://webkrauts.de/sites/all/themes/webkrauts/img/webkrauts_bildmarke_small.png';  
  12.   var tag = 'WKAK2015';
  13.  
  14.   // Warten, bis das promise erfolgreich war, und dann die Nachricht anzeigen
  15.   event.waitUntil(  
  16.     self.registration.showNotification(title, {  
  17.       body: body,  
  18.       icon: icon,  
  19.       tag: tag  
  20.     })  
  21.   );  
  22. });
  23.  
  24. // Was passiert, wenn die Nachricht angeklickt wird?
  25. self.addEventListener('notificationclick', function(event) {  
  26.  
  27.   // Hinweis für die Konsloe
  28.   console.log('On notification click: ', event.notification.tag);  
  29.   // Android schließt die Nachricht nicht auf Touch/Klick
  30.   // http://crbug.com/463146  
  31.   event.notification.close();
  32.  
  33.   // Wir rufen die Seite auf. Falls sie schon geöffnet ist, fokussieren wir den Tab, sonst öffnen wir sie neu.
  34.   event.waitUntil(
  35.     clients.matchAll({  
  36.       type: "window"  
  37.     })
  38.     .then(function(clientList) {  
  39.       for (var i = 0; i < clientList.length; i++) {  
  40.         var client = clientList[i];  
  41.         if (client.url == 'http://webkrauts.de/serien/adventskalender/2015' && 'focus' in client)  
  42.           return client.focus();  
  43.       }  
  44.       if (clients.openWindow) {
  45.         return clients.openWindow('http://webkrauts.de/serien/adventskalender/2015');  
  46.       }
  47.     })
  48.   );
  49. });

Es wird Zeit, unsere Push-Nachricht zu versenden. Dazu fehlt uns zunächst noch die Funktion zum Versenden, die wir schon an mehreren Stellen in unseren Funktionen verwendet haben. Hier rufen wir eine Senderoutine auf, in diesem Beispiel in PHP geschrieben. Ähnliche Möglichkeiten bestehen aber natürlich auch in anderen Programmiersprachen. Dabei übermitteln wir die Identifikation unseres Browsers für das Abo aus dem Push Manager. Um hier mit der reinen ID arbeiten zu können, entfernen wir zuvor die URL zur Google API, die Chrome mitliefert. Das wäre zwar in diesem Fall nicht nötig, weil wir sie später wieder anfügen. Wer aber anstelle der Google API seine eigene Pushinfrastruktur verwenden will, kann mit der Google-URL nichts anfangen.

  1. //Die Funktion zum Senden der komplexen Notification
  2. function sendNotification(e) {
  3.   //Die registration_id extrahieren
  4.   e = e.replace("https://android.googleapis.com/gcm/send/", "");
  5.   // AJAX-Aufruf an die Sendefunktion
  6.   // Normalerweise will man dies vermutlich nicht für den Enduser einrichten, sondern serverseitig lösen.
  7.   $.ajax({
  8.     data: 'id=' + e,
  9.     url: 'send.php',
  10.     method: 'GET'
  11.   });
  12. }

Um die Sendung über die Google API vornehmen zu können, benötigen wir wie angekündigt einen API-Schlüssel. Diesen generieren wir in der Developer Console (s.o.). Dazu dient im linken Menü der Punkt »APIs und Authentifizierung«. Zunächst muss im Bereich »APIs« unter »Mobile APIs« der Punkt »Cloud Messaging for Android« ausgwählt werden. Hier kann diese spezielle API aktiviert werden. Anschließend kann im Bereich »Zugangsdaten« ein neuer API-Schlüssel generiert werden. Hierzu klickt Ihr oben links auf den Button »Anmeldedaten hinzufügen« und wählt »API-Schlüssel« aus. Wir arbeiten in diesem Fall mit einem serverseitigen Skript. Daher ist im folgenden Dialog »Server-Schlüssel« auszuwählen. Zu Testzwecken könnt ihr hier einen beliebigen Namen eingeben und die Limitierung der berechtigten IP-Adressen freilassen. In einer Produktionsumgebung empfiehlt es sich natürlich, entsprechende Beschränkungen zu aktivieren und nur Zugriffe vom richtigen Server zu erlauben. Den generierten Schlüssel können wir jetzt in unser Skript einfügen.

Ein Screenshot der Developer-Konsole von Google während der Erstellung eines API-Schlüssels.
Zur Authentifizierung muss in der Google-Developer-Konsole ein API-Schlüssel generiert werden.

Das Skript entnimmt einer GET-Variable die übermittelte Browser-ID aus dem Push Manager und bereitet die nötigen Elemente für den Versand vor. Um zu demonstrieren, dass die Seite zum Empfang der Nachricht nicht offen sein muss, wird hier eine Sendeverzögerung von zehn Sekunden eingefügt. Ihr habt also beim Ausprobieren genug Zeit, die Seite oder sogar den ganzen Browser zu schließen, nachdem ihr den Sende-Button angeklickt habt. Dann folgt der eigentliche Versand durch cURL, ein Kommandozeilenprogramm zum Versenden von Dateien. In diesem Fall handelt es sich um eine JSON-Datei.

  1. // API-Schlüssel für die Google Console
  2. define( 'API_ACCESS_KEY', 'HIER GEHÖRT EUER API SCHLÜSSEL HIN' );
  3.  
  4. // Die Empfänger-ID übernehmen
  5. $registrationIds = array( $_GET['id'] );
  6.  
  7. // Felder definieren
  8. $fields = array( 'registration_ids' => $registrationIds);
  9.  
  10. // Kopfzeilen definieren
  11. $headers = array(
  12.   'Authorization: key=' . API_ACCESS_KEY,
  13.   'Content-Type: application/json'
  14. );
  15.  
  16. // Verzögern des Versands, um beispielsweise den Tab zu schließen
  17. sleep(10);
  18.  
  19. // Versenden
  20. $ch = curl_init();
  21. curl_setopt( $ch,CURLOPT_URL, 'https://android.googleapis.com/gcm/send' );
  22. curl_setopt( $ch,CURLOPT_POST, true );
  23. curl_setopt( $ch,CURLOPT_HTTPHEADER, $headers );
  24. curl_setopt( $ch,CURLOPT_RETURNTRANSFER, true );
  25. curl_setopt( $ch,CURLOPT_SSL_VERIFYPEER, false );
  26. curl_setopt( $ch,CURLOPT_POSTFIELDS, json_encode( $fields ) );
  27. $result = curl_exec($ch );
  28. curl_close( $ch );
  29.  
  30. // Im Bedarfsfall zum Debuggen das Ergebnis ausgeben
  31. //echo $result;

Wenn wir dies alles in Kombination in unserem Beispiel ausprobieren, kommt in Chrome auf dem Desktop und auch auf Android eine entsprechende Nachricht an. Dies sollte sogar funktionieren, wenn ihr die Seite schließt, und sogar falls der Browser geschlossen wurde, solange noch eine Instanz im Hintergrund läuft. Ansonsten wird die Nachricht zugestellt, sobald der Browser wieder online geht.

Auch unter Android funktioniert diese Funktion und integriert sich in das Mitteilungscenter und zwar auch, wenn der Browser geschlossen ist.

Eine Push-Nachricht im Benachrichtigungscenter eines Androidmobiltelefons.
Die Push-Nachricht taucht im Mitteilungscenter des Android-Betriebssystems auf.

Funktionsfähige Demo ansehen und ausprobieren

Fazit

Während die Implementierung für Notifications wie oben gezeigt recht weit gediehen ist, sieht es bei einigen unterstützenden Webtechnologien noch nicht so rosig aus. Erst die Verwendung von Push-Technik hebt Notifications von den üblichen modalen Dialogen oder JavaScript-Meldungen ab. Dies funktioniert aber bisher nur in Desktop-Chrome, Android-Chrome und Desktop-Safari. Trotz des großen Potentials steht eine flächendeckende Verbreitung vorerst noch in den Sternen.

Die Kommentare sind geschlossen.