Push-Web
Push-Nachrichten im Browser empfangen, Teil 1
Auf dem Smartphone sind Push-Benachrichtigungen alltäglich. Nun können auch die Browser solche Nachrichten empfangen und mit den Mitteln des Betriebssystems ausgeben – und verkleinern so die Lücke zwischen Apps und Webanwendungen.
Mit Benachrichtigungen können Webseiten aus dem Browserfenster ausbrechen und (hoffentlich) wichtige und interessante Hinweise geben: Messenger-News von Freunden, Eilmeldungen und Sportergebnisse, Aktienkurse, Sonderangebote und vieles mehr. Dieser Artikel zeigt anhand eines Beispiels, wie Browser Push-Nachrichten verarbeiten. Den Beispiel-Code und weiterführende Informationen finden Sie unter ct.de/yy3m.
Eine Schnittstelle für einfache Benachrichtigungen – das Notification API – gibt es schon seit ein paar Jahren. Sie einzusetzen, verlangt dem Webentwickler nicht allzu viel ab:
<form action="#">
<textarea></textarea>
<button>Benachrichtigen</button>
</form>
<script src="notification.js">
</script>
Ein Skript soll nun auf Knopfdruck den Inhalt der <textarea> als Benachrichtigung aufpoppen lassen:
document.querySelector('button').
addEventListener('click', ev => {
ev.preventDefault();
if (!('Notification' in window))
throw new Error('Kein Push!');
Notification.requestPermission
if (permission !== 'granted')
{
alert('Keine Erlaubnis!');
return;
}
const msg = new
Notification('Nachricht', {
body: document
.querySelector('textarea')
.value, icon: 'ct.png'
});
msg.onclick = ev =>
alert('Nachricht angeklickt!');
});
});
Auf ein click-Ereignis des Buttons hin unterdrückt preventDefault() erst einmal den Wunsch des Browsers, das Formular abzusenden. Nach einer Prüfung, ob der Browser das Notification API beherrscht, fordert requestPermission() eine Genehmigung dafür an, falls der Benutzer diese nicht bereits bei einem früheren Besuch erteilt hat – denn natürlich darf nicht jede beliebige Website einfach so das Nervenkostüm des Besuchers mit sinnlosen Nachrichten penetrieren. Sie können die danach auszuführende Aktion wie hier als Callback oder über ein Promise (requestPermission().then(…)) angeben.
Die Benachrichtigung selbst entsteht durch new Notification(). Notification() erwartet zwei Argumente: einen String als Titel und ein Objekt mit den Details. Darin sollten Sie insbesondere einen body-Text und ein icon im PNG-Format angeben. Manche Browser zeigen auch ein image an oder spielen eine selbst definierte Vibration (vibrate) ab. Vier Ereignis-Handler begleiten die Benachrichtigung durch ihren Lebenszyklus: onshow, onclick, onclose und onerror.
Die Benachrichtigungen erscheinen in Form eines Systemdialogs außerhalb des Browsers. Unter Windows poppen sie üblicherweise in der rechten unteren Ecke des Desktops auf. Weder der Tab noch das Browserfenster muss im Vordergrund sein, damit das klappt. Wenn Sie es nicht glauben, verzögern Sie die Benachrichtigung, indem Sie einfach den Inhalt der obigen Funktion in ein setTimeout() setzen – das gibt Ihnen Zeit, vor dem Empfang der Nachricht Fenster oder Tab zu wechseln.
Das Anklicken der Benachrichtigung holt die dazugehörige Webseite in den Vordergrund; durch onclick lässt sich das wie im Beispiel mit zusätzlichen Aktionen verbinden. All das funktioniert in modernen Desktop-Browsern (also nicht im Internet Explorer), nicht jedoch in den gängigen Mobil-Browsern.
Gepusht
So weit, so gut – aber etwas Entscheidendes fehlt. Für sich genommen ist das Notification API nämlich abhängig davon, dass der Benutzer die betreffende Webseite geöffnet hat. Außer für Power-Surfer mit einer zwei- bis dreistelligen Zahl offener Tabs bringen diese Benachrichtigungen daher so gut wie nichts, weshalb ihnen auch kein großer Erfolg beschieden war.
Was man wirklich haben möchte, sind Push-Benachrichtigungen, wie man sie von Mobil-Apps kennt: Nach seiner Einwilligung erhält der Nutzer Benachrichtigungen, wann immer es dem Anbieter der Webseite sinnvoll erscheint. Aber wie sollte das im Browser funktionieren? Selbst wenn der Push-Dienst auf magische Weise wüsste, an welchen Rechner er seine Nachrichten schicken soll, kann die dazugehörige Webseite keinen Code ausführen, ohne geöffnet zu sein.
Erstaunlicherweise ist aber genau das inzwischen möglich. Ausgangspunkt dafür ist eine Weiterentwicklung des oben verwendeten HTML-Formulars:
<form action="#">
<label>
Schlüssel:
<input type="text">
</label>
<button disabled>
Push-Nachrichten nicht möglich
</button>
<output></output>
</form>
<script src="push.js"></script>
Der vorerst deaktivierte <button> dient hier zum Abonnieren der Push-Nachrichten, die später vom Server kommen sollen.
ServiceWorker sind eine der technischen Neuerungen, die Push-Nachrichten ermöglichen. Die in JavaScript geschriebenen ServiceWorker agieren als eine Art Proxy zwischen dem Browser und dem Server, der sie ursprünglich installiert hat. Sie nehmen Anfragen entgegen, auch wenn die betreffende Webseite nicht geöffnet ist.
Zum Kasten: Progressive Web Apps
ServiceWorker sind das Herzstück der sogenannten Progressive Web Apps (PWA), die Websites einige der Möglichkeiten eröffnen, die bisher nur Smartphone-Apps zur Verfügung standen – zum Beispiel individuell programmierbares Caching mit Offline-Funktion oder eben der Empfang von Push-Benachrichtigungen [1]. Außer bei Tests auf dem Localhost setzen sie eine HTTPS-Verbindung voraus.
Somit wird die erste Aufgabe sein, einen ServiceWorker zu installieren:
'use strict';
const btnTexts = [
'Abonnieren',
'Abo beenden'
];
const btn = document.
querySelector('button');
let worker = null;
let isSubscribed = false;
if ('PushManager' in window) {
navigator.serviceWorker.
register('worker.js')
.then(reg => { /* to do ... */ })
.catch(err => console.error(err));
}
Zunächst definieren Sie Button-Beschriftungen für Beginn und Ende eines Push-Abonnements und greifen auf den <button> zu. Die Variable worker speichert die Registrierung des ServiceWorkers, isSubscribed hält den Abo-Status fest.
Um zu testen, ob der Browser die notwendigen Fähigkeiten besitzt, fragt der Code nach der PushManager-Schnittstelle des Push-API. Im Augenblick beantworten nur Chromium und Firefox auf Desktop und Mobilgeräten diese Frage positiv, aber Safari und Edge inklusive dem Microsoft Store sind gerade dabei nachzuziehen. Der Browser registriert nun die Skript-Datei worker.js als ServiceWorker; für diese genügt vorerst eine leere Datei. Wenn das geklappt hat, gibt das Promise die Worker-Registrierung zurück. Damit können Sie den Status des Push-Abos abfragen:
reg => {
worker = reg;
btn.disabled = false;
worker.pushManager.getSubscription()
if (subscription === null) {
btn.textContent = btnTexts[0];
} else {
isSubscribed = true;
btn.textContent = btnTexts[1];
}
});
}
Das sichert die ServiceWorker-Registrierung in der globalen Variablen worker und macht den Abo-Button klickbar. Mit getSubscription() prüfen Sie, ob der Nutzer bereits Push-Benachrichtigungen für die aktuelle Domain abonniert hat – was sich in der Button-Beschriftung und in der Variablen isSubscribed niederschlägt. Die Seite sollte nun einen klickbaren Button „Abonnieren“ anzeigen.
Probe-Push
Mit den Entwicklerwerkzeugen können Sie bereits jetzt eine Nachricht absetzen – aber nicht empfangen. Dafür muss sich nämlich erst der Worker an die Arbeit machen. Schreiben Sie folgenden Code in worker.js:
'use strict';
self.addEventListener('push', ev => {
const title = 'Nachricht!';
let text = 'Text ...';
if (ev.data !== null)
text = ev.data.text();
ev.waitUntil(self.registration.
showNotification(title,
{body: text}));
});
self bezieht sich hier auf den globalen Scope in ServiceWorkern und ersetzt hier window. Beim Empfang einer Push-Nachricht tritt die zur ServiceWorker-Registrierung gehörende Methode showNotification() in Aktion, die den gleichen Regeln folgt wie das Ihnen bereits bekannte Notification API. waitUntil() ist eine nicht zwingend nötige Vorsichtsmaßnahme, mit welcher der ServiceWorker den Browser auffordert, nicht vorzeitig beendet zu werden.
Wie im vorigen Beispiel können Sie auch eine Aktion auslösen, wenn der Benutzer auf die Benachrichtigung klickt. Ohne Zugriff auf window ist das allerdings ein wenig komplizierter:
self.addEventListener
('notificationclick', ev => {
ev.notification.close();
ev.waitUntil(
self.clients.
openWindow('https://ct.de/')
);
});
Das notificationclick-Ereignis erlaubt Zugriff auf die Benachrichtigung. Um eine URL zu öffnen, brauchen Sie die openWindow()-Methode der Clients-Schnittstelle, die ServiceWorker in der Variablen self.client bereitstellt. Mehr gibt es im ServiceWorker nicht zu tun.
In Chromium-Browsern können Sie nun im Reiter „Application“ der Entwicklerwerkzeuge eine Push-Nachricht absetzen. In Desktop-Browsern sollte diese rechts unten aufpoppen. Firefox verschickt von der Seite about:debugging#workers aus eine textlose Nachricht.
Schlüsseltausch
Im nächsten Schritt soll das Frontend das Abonnement der Push-Nachrichten steuern. Ergänzen Sie push.js, um Klicks auf den Button zu verarbeiten:
button.addEventListener('click',
ev => {
ev.preventDefault();
if (worker === null) return;
if (isSubscribed) {
// unsubscribe ...
} else {
// subscribe ...
}
});
Ist es zuvor nicht gelungen, einen ServiceWorker zu installieren (worker === null), bricht der Handler ab. Statt das Formular abzusenden (preventDefault()), soll das Skript Push-Nachrichten abonnieren oder ein bestehendes Abo kündigen – je nach dem Wert von isSubscribed.
Für den nächsten Schritt brauchen Sie ein wenig Hintergrundwissen zu den komplizierten Abläufen beim Versand von Push-Nachrichten. Diese schickt der betreffende Webdienst dem Nutzer nämlich nicht direkt, sondern über eine dritte Partei. Nach dem Start nehmen Browser Kontakt zu einem fest verdrahteten Push-Dienst auf und halten die TCP-Verbindung. Auf diese Weise hat der Push-Dienst jederzeit die Möglichkeit, Daten an den Nutzer zu senden. Der Push-Dienst für alle Chromium-Browser sendet von fcm.googleapis.com aus, das Mozilla-Gegenstück liegt unter updates.push.services.mozilla.com. Push-Dienste haben eine nach RFC 8030 genormte Schnittstelle.
Beim Abschluss eines Push-Abonnements funkt der Browser seinen Push-Dienst an und erhält als Antwort eine lange individuelle „Endpunkt“-URL. Unter dieser URL empfängt der Push-Dienst Nachrichten vom Anwendungsserver, die er an den betreffenden Nutzer pusht.
Ursprünglich war gedacht, dass die Endpunkt-URL für den Versand ausreicht (bei Mozillas Push-Dienst ist das auch noch möglich), doch dann hat man mehrere Verfahren zur Verschlüsselung und Authentifizierung nachgetragen. Deshalb muss der Browser beim Abschluss des Abonnements den öffentlichen Schlüssel des Anwendungsservers an den Push-Dienst übergeben (siehe Abbildung auf dieser Seite).
Solange Sie noch kein eigenes Backend haben, können Sie den Push Companion (https://web-push-codelab.glitch.me) benutzen. Der einzige Unterschied zu einem echten Anwendungs-Backend ist, dass die Prozesse nicht automatisch, sondern von Hand laufen. Wenn Sie diese Seite öffnen, finden Sie darin einen ungefähr 65 Byte langen öffentlichen Schlüssel. Diesen kopieren Sie vor Abschluss des Abonnements ins Eingabefeld des eingangs definierten Formulars.
Das Format dieses Schlüssels ist eine leicht modifizierte Variante von Base64, die in URLs keine Probleme verursacht. Der Push-Dienst erwartet den Schlüssel dagegen in Form eines typisierten Arrays – Sie müssen also konvertieren:
function urlB64ToUint8(b64String) {
const padding = '='.repeat(
(4 - b64String.length % 4) % 4);
const b64 = (b64String + padding)
.replace(/\-/g, '+')
.replace(/_/g, '/');
const raw = atob(b64);
const outputArray =
new Uint8Array(raw.length);
for(let i = 0; i < raw.length; ++i)
outputArray[i] =
raw.charCodeAt(i);
return outputArray;
}
Diese Funktion wandelt das URL-sichere Base64 in normales um, extrahiert den Bytecode und schreibt diesen in ein Array von vorzeichenlosen 8-Bit-Integern.
Abonnieren
Damit ist alles bereit zum Abonnieren:
const pubkey =
document.querySelector('input');
const output =
document.querySelector('output');
btn.addEventListener('click', ev => {
if (isSubscribed) {
// ...
} else {
worker.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey:
urlB64ToUint8(pubkey.value)
})
.then(subscr => {
output.textContent =
JSON.stringify(subscr);
btn.textContent = btnTexts[1];
isSubscribed = true;
})
}
});
Zuständig für das Abonnement ist die Methode pushManager.subscribe() der ServiceWorker-Registrierung. Als Argument erwartet sie ein Objekt mit zwei Optionen: den konvertierten applicationServerKey und die Zusage, dass alle Push-Daten für den Benutzer sichtbar werden (userVisibleOnly). Unsichtbare Push-Daten wurden zwar angedacht, doch die Browser lassen dies aus Sicherheitsgründen nicht zu.
Wenn alles klappt, erhält das Promise ein subscription-Objekt zurück. Dessen Inhalt müssen Sie an das Backend durchreichen. In diesem Fall ist der Push-Companion das Backend und Sie kommunizieren damit via Copy und Paste. Daher gibt der Code die subscription als String im <output>-Element aus. Schließlich gilt es noch, den Button-Text und den isSubscribed-Status zu aktualisieren.
Bevor Sie sich ans Testen machen, vervollständigen Sie noch kurz den Unsubscribe-Teil:
if (isSubscribed) {
worker.pushManager.getSubscription()
.then(subscr => {
if (subscr)
subscr.unsubscribe();
})
.then(() => {
output.textContent = '';
button.textContent = btnTexts[0];
isSubscribed = false;
});
} else {...}
getSubscription() kennen Sie ja schon. Auf das von diesem Promise zurückgegebene subscription-Objekt wenden Sie die unsubscribe()-Methode an. Zuletzt müssen Sie ähnlich wie vorhin das HTML und isSubscribed aktualisieren.
Versandfertig
Wenn Sie nun einen vom Push Companion angezeigten öffentlichen Schlüssel ins Eingabefeld kopieren und im Browser auf „Push-Nachrichten abonnieren“ klicken, erscheint im <output> JSON-Code. Dieser enthält die URL des endpoint, eine expirationTime für die Gültigkeit des Abonnements (in unseren Experimenten war das stets null) sowie zwei vom Browser erzeugte Base64-Strings: den vom Browser erzeugten öffentlichen Schlüssel p256dh, der später die Nachricht chiffrieren wird, und das auth-Geheimnis für die Authentifizierung.
Diese Ausgabe fügen Sie in den Push Companion ein. Achten Sie darauf, dass dort der gleiche Schlüssel eingestellt ist wie in Ihrer Seite. Auf Knopfdruck schickt die Seite eine Push-Nachricht an den Push-Service-Endpoint, der sie an den von Ihnen programmierten ServiceWorker weiterreicht.
Achtung: Falls Sie beim Herumprobieren den Empfang von Benachrichtigungen zu oft ablehnen, verwirft der Browser diese kommentarlos. Chromium-Browser fragen nur dreimal nach – danach müssen Sie die Website-spezifischen Einstellungen ändern.
Wenn Sie selbst Push-Nachrichten verschicken wollen, hilft Ihnen der Push Companion nicht weiter. Ein eigenes Backend zu bauen, ist aber kein Spaziergang: Die dort notwendigen Prozesse nennt selbst Googles Tutorial „extrem fummelig“, vor allem, weil sich Probleme nur schwer diagnostizieren lassen. Wir zeigen Ihnen im zweiten Teil des Push-Tutorials in einem folgenden Heft, wie Sie das mit dem auf Node.js aufsetzenden Tool web-push meistern. (jo@ct.de)
Beispiel-Code:ct.de/yy3m