Zum Hauptinhalt springen

Push Notifications

In ChatGPT öffnen
26.02
Java API

Push-Benachrichtigungen können Benutzer sogar erreichen, wenn eine App nicht geöffnet ist. Der Browser abonniert einmal, die App speichert das Abonnement und der Server nutzt es, um Benachrichtigungen zu liefern, wenn ein Ereignis eintritt. Push verwaltet das Abonnieren und Abbestellen im Browser. Auf dem Server sendet PushSender eine PushMessage an ein gespeichertes Abonnement.

Einrichtung und Voraussetzungen

Push-Benachrichtigungen werden von einem separaten Modul bereitgestellt. Fügen Sie es Ihrer App hinzu:

pom.xml
<dependency>
<groupId>com.webforj</groupId>
<artifactId>webforj-push</artifactId>
</dependency>

Push-Benachrichtigungen erfordern:

  • Eine Servlet-Bereitstellung, wie Jetty, Spring Boot oder eine WAR-Datei.
  • Ein Schlüsselpaar, das weiter unten generiert wird und von der Bereitstellung verwendet wird, um Benachrichtigungen zu signieren.
  • Eine sichere Herkunft. Browser lehnen Abonnements ab, die über andere Protokolle als https bereitgestellt werden, außer von localhost während der Entwicklung.
Sichere Ursprünge

Weitere Informationen zu sicheren Kontexten und warum sie wichtig sind, finden Sie in der MDN-Dokumentation zu sicheren Kontexten.

Schlüssel generieren

Push-Dienste akzeptieren nur Benachrichtigungen, die von der Bereitstellung signiert sind, bei der der Browser abonniert wurde. Führen Sie das Build-Plugin einmal für jede Bereitstellung aus, um sein Schlüsselpaar zu generieren:

mvn webforj:push-keys

Der Befehl gibt drei Konfigurationszeilen aus. Fügen Sie diese ohne die Anführungszeichen in die application.properties ein oder kopieren Sie sie wie gedruckt in die webforj.conf. Ersetzen Sie das Subjekt durch die Kontaktadresse der Bereitstellung. Es muss sich um eine mailto: oder https:// Adresse handeln, über die Push-Dienste den Betreiber kontaktieren können.

application.properties
webforj.push.public-key=...
webforj.push.private-key=...
webforj.push.subject=mailto:ops@example.com
EigenschaftErklärung
webforj.push.public-keyDie öffentliche Hälfte des Schlüsselpaares, die von der Bereitstellung verwendet wird, um Benachrichtigungen zu signieren
webforj.push.private-keyDie private Hälfte des Schlüsselpaares. Halten Sie sie wie jedes andere Geheimnis aus der Quellkontrolle fern
webforj.push.subjectDie Kontaktadresse der Bereitstellung. Es muss sich um eine mailto: oder https:// Adresse handeln, über die Push-Dienste den Betreiber erreichen können

Die App liest diese Eigenschaften beim Start. Wenn die Konfiguration nur einige von ihnen enthält, schlägt der Start fehl und meldet, welche Eigenschaften fehlen.

Schlüsselrotation

Jeder Browser abonniert ein Schlüsselpaar. Wenn sich die Schlüssel ändern, lehnt der Push-Dienst bestehende Abonnements ab. Der nächste subscribe()-Aufruf in jedem Browser ersetzt sein Abonnement.

Wie es funktioniert

Der Prozess hat drei Schritte:

  1. Abonnieren. Aus einer Ansicht fordert Push.getCurrent().subscribe() die Erlaubnis des Benutzers an und gibt ein PushSubscription zurück, das die Adresse des Browsers identifiziert.
  2. Speichern. Die App speichert das Abonnement mit seinen Daten und verknüpft es mit dem entsprechenden Benutzer.
  3. Senden. Später, von jedem Thread aus, übergibt PushSender.send(subscription, message) die Nachricht an den Push-Dienst des Browseranbieters. Der Dienst zeigt die Benachrichtigung an, unabhängig davon, ob die App geöffnet ist oder nicht.
Push.getCurrent().subscribe().thenAccept(subscriptions::save);

sender.send(subscription,
PushMessage.create("Bestellung versendet").setUrl("/orders/42").build());

Die folgenden Abschnitte erklären, was der Browser anzeigt und wie man Fehler bei jedem Schritt behandelt.

Instanz

Rufen Sie die Push-Instanz für die aktuelle Umgebung ab:

import com.webforj.push.Push;

Push push = Push.getCurrent();

if (Push.isPresent()) {
// ...
}

Push.ifPresent(p -> {
// ...
});

Browsers abonnieren

Rufen Sie subscribe() als Reaktion auf eine Benutzeraktion auf, zum Beispiel beim Klicken auf eine Schaltfläche "Benachrichtigungen aktivieren". Das zurückgegebene PendingResult wird mit dem PushSubscription des Browsers abgeschlossen. Wenn der Browser nicht abonnieren kann, wird es mit einer WebforjPushException außergewöhnlich abgeschlossen.

PendingResult<PushSubscription> request = Push.getCurrent().subscribe();
request.thenAccept(subscription -> {
subscriptions.save(subscription);
});
request.exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable.getCause();
PushStatus status = error.getStatus();
String message = error.getMessage();

return null;
});

Wenn der Browser bereits abonniert ist, gibt der erneute Aufruf von subscribe() das bestehende Abonnement zurück. Sie können es daher sicher bei jedem Besuch aufrufen.

Berechtigung des Browsers

Der erste Aufruf von subscribe() fordert den Benutzer um Erlaubnis an. Der Browser zeigt dieses Eingabefeld an, es gehört nicht zur UI der App. Da Browser das Eingabefeld nur als Reaktion auf eine Benutzeraktion anzeigen, rufen Sie subscribe() aus einem Klick-Listener und nicht aus dem Konstruktor der Ansicht auf.

Wenn der Benutzer das Eingabefeld blockiert, kann die App nicht erneut um Erlaubnis für diesen Ursprung bitten.

Abonnements speichern

Ein Abonnement stellt die Adresse eines Browsers dar und gehört auf den Server. Speichern Sie es mit den Daten der App, wobei Sie seinen Endpunkt als Schlüssel verwenden. Fügen Sie alle Informationen hinzu, die die App benötigt, um später die entsprechenden Browser auszuwählen, z. B. den zugehörigen Benutzer. Jedes Abonnement enthält drei Textwerte:

WertBedeutung
getEndpoint()Die Liefer-URL, die vom Push-Dienst des Browseranbieters zugewiesen wurde
getP256dh()Der öffentliche Schlüssel des Browsers
getAuth()Das Authentifizierungsgeheimnis des Browsers

Ein Benutzer, der sich von zwei Browsern anmeldet, hat zwei Abonnements. Löschen Sie ein Abonnement, wenn sein Browser sich abmeldet oder wenn ein Senden meldet, dass es abgelaufen ist. Siehe Fehlerstatus.

Ein Abonnement wiederherstellen

getSubscription() gibt das aktuelle Abonnement des Browsers zurück oder ein leeres Ergebnis, wenn keines vorhanden ist. Verwenden Sie es, um die Kopie des Servers zu synchronisieren, zum Beispiel nachdem der Speicher der App zurückgesetzt wurde:

Push.getCurrent().getSubscription().thenAccept(existing -> {
existing.ifPresent(subscriptions::save);
});

Über PushPermission berichtet getPermission(), ob der Benutzer die Benachrichtigung genehmigt, abgelehnt hat oder noch nicht auf die Eingabeaufforderung geantwortet hat. Verwenden Sie dieses Ergebnis, um die Schaltfläche "Benachrichtigungen aktivieren" auszublenden, wenn ein Klick darauf keine Wirkung hätte.

Abmelden

unsubscribe() kündigt das Abonnement des Browsers. Es wird mit dem entfernten Abonnement abgeschlossen, sodass die App ihre gespeicherte Kopie löschen kann, oder mit einem leeren Ergebnis, wenn der Browser kein Abonnement hatte.

Push.getCurrent().unsubscribe().thenAccept(removed -> {
removed.ifPresent(subscriptions::delete);
});

Benachrichtigungen senden

PushSender sendet eine PushMessage an ein gespeichertes Abonnement. Es signiert die Nachricht mit den Schlüsseln der Bereitstellung und übergibt sie an den Push-Dienst des Browseranbieters. Dieser Dienst weckt den Browser und zeigt die Benachrichtigung an. Da der Vorgang den aufrufenden Thread niemals blockiert, können Sie ihn aus einem Klick-Listener, einem geplanten Job oder einem Anforderungs-Handler aufrufen.

Nachdem die Eigenschaften konfiguriert sind, ist der Sender als Bean verfügbar, die Sie in Ansichten, Diensten und geplanten Jobs injizieren können. Um ihn zu ersetzen, definieren Sie Ihre eigene PushSender-Bean.

@Route("/orders")
public class OrdersView extends Composite<FlexLayout> {

public OrdersView(PushSender sender, PushSubscriptions subscriptions) {
// ...
}
}

Ohne Spring, new PushSender() liest die Schlüssel aus der Konfiguration der App. Erstellen Sie den Sender in einem App-Thread, entweder in einer Ansicht oder in App.run(), und verwenden Sie ihn dann von jedem Thread aus. Alle Sender teilen sich einen Verbindungs-Pool zu den Push-Diensten, sodass es keine Kosten verursacht, einen überall dort zu erstellen, wo er benötigt wird.

Für Benachrichtigungen, die später oder nachdem der Benutzer gegangen ist, gesendet werden müssen, verwenden Sie einen Timer auf dem Server wie Springs TaskScheduler. Verwenden Sie keinen Seiten-Timer wie Interval, da er stoppt, wenn der Tab geschlossen wird.

Eine Nachricht erstellen

Erstellen Sie eine Nachricht mit ihrem Titel, und konfigurieren Sie dann jede andere Option im Builder:

PushMessage message = PushMessage.create("Bestellung versendet")
.setBody("Bestellung #42 ist auf dem Weg")
.setIcon("icons://icon-192x192.png")
.setUrl("/orders/42")
.setActions(List.of(new PushAction("track", "Verfolgen", "/orders/42/tracking")))
.build();

PendingResult<Void> sent = sender.send(subscription, message);
sent.thenAccept(v -> status.setText("Gesendet"));
sent.exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable;
status.setText(error.getStatus() + ": " + error.getMessage());

return null;
});

send() gibt sofort zurück. Das PendingResult wird abgeschlossen, wenn der Push-Dienst die Nachricht annimmt, oder es wird außergewöhnlich abgeschlossen, wenn der Dienst sie nicht annimmt. Wenn send() in einem App-Thread aufgerufen wird, z. B. aus einem Listener, werden dessen Rückrufe in diesem Thread ausgeführt und können Komponenten aktualisieren. Wenn die Sitzung, die send() aufgerufen hat, endet, bevor die Antwort eintrifft, werden die Rückrufe nicht ausgeführt, aber die Benachrichtigung wird trotzdem geliefert.

Ein Versand wartet bis zu 30 Sekunden auf den Push-Dienst, bevor er mit UNREACHABLE fehlschlägt. Verwenden Sie setTimeout(Duration), um das Timeout für jeden Sender zu ändern.

OptionEffekt
setBodySetzt den Text, der unter dem Titel angezeigt wird
setIconLegt das Bild fest, das mit der Benachrichtigung angezeigt wird. Es akzeptiert absolute URLs und die Protokolle icons:// und ws://. Siehe Assets. Es akzeptiert nicht das Protokoll context://, da Push-Dienste eine Nachricht auf 4 KB beschränken
setUrlLegt die Seite fest, die öffnet wird, wenn der Benutzer auf die Benachrichtigung klickt. Relative URLs werden gegen die App-Wurzel aufgelöst. Wenn keine URL festgelegt ist, wird die App-Wurzel geöffnet
setActionsLegt die Schaltflächen fest, die auf der Benachrichtigung angezeigt werden, mit einer separaten URL für jede Schaltfläche. Siehe Browserunterstützung
setTagLegt ein identifizierendes Tag fest. Wenn eine angezeigte Benachrichtigung dasselbe Tag hat, ersetzt die neue Benachrichtigung sie
setSilentZeigt die Benachrichtigung ohne Ton oder Vibration an
setTimeToLiveLegt fest, wie lange der Push-Dienst die Nachricht für ein offline Gerät aufbewahrt, bis zu vier Wochen
setUrgencyVerwendet PushUrgency, um dem Gerät zu ermöglichen, Nachrichten mit niedriger Dringlichkeit zu verzögern und Batterielebensdauer zu sparen
setTopicErsetzt eine Nachricht, die noch beim Push-Dienst wartet, wenn beide Nachrichten dasselbe Thema haben. Themen können maximal 32 Zeichen enthalten, die in einer URL sicher sind

Wenn ein Tab die Seite bereits anzeigt, fokussiert das Klicken auf die Benachrichtigung die App. Ansonsten wird die Seite in einem neuen Tab geöffnet. Das Klicken auf eine Benachrichtigungs-Schaltfläche öffnet ihre URL auf die gleiche Weise.

Eine Benachrichtigung pro Nachricht

Jede Nachricht zeigt eine Benachrichtigung an. Da Browser eine Seite nicht für eine Nachricht wecken, die nichts anzeigt, kann Push nicht für stille Datenupdates verwendet werden.

Fehlerstatus

Wenn subscribe() oder send() fehlschlägt, berichtet sein PendingResult eine WebforjPushException. PushStatus identifiziert den Grund:

StatusWannWas zu tun ist
PERMISSION_DENIEDDer Benutzer hat Benachrichtigungen für die App blockiertErklären Sie, wo der Benutzer Benachrichtigungen in den Browsereinstellungen zulassen kann
UNSUPPORTEDPush wird vom Browser nicht unterstützt, die Seite befindet sich nicht in einem sicheren Kontext oder die App ist nicht als Servlet bereitgestelltVerstecken Sie die Funktion
NOT_CONFIGUREDMindestens eine webforj.push.*-Eigenschaft fehlt oder ist unvollständigGenerieren Sie die Schlüssel und konfigurieren Sie alle drei Eigenschaften
SUBSCRIPTION_EXPIREDDer Push-Dienst erkennt das Abonnement nicht mehr, da der Benutzer sich abgemeldet oder den Browser neu installiert hatEntfernen Sie das gespeicherte Abonnement
REJECTEDDer Push-Dienst hat die Nachricht abgelehnt; getStatusCode() enthält die AntwortÜberprüfen Sie die Schlüssel und die Nachrichtengröße
UNREACHABLEDer Push-Dienst hat vor dem Timeout nicht reagiertVersuchen Sie es später erneut
UNKNOWNDer gespeicherte Endpunkt ist keine gültige URL, oder das Abonnement oder die Nachricht konnten nicht kodiert werdenÜberprüfen Sie das gespeicherte Abonnement

Entfernen Sie abgelaufene Abonnements bei jedem Versand:

sender.send(subscription, message).exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable;
if (error.getStatus() == PushStatus.SUBSCRIPTION_EXPIRED) {
subscriptions.delete(subscription);
}

return null;
});
Ablauf kommt eine Nachricht zu spät

Push-Dienste deregistrieren Abonnements träge. Sie akzeptieren immer noch die erste Nachricht, nachdem sich ein Benutzer abgemeldet hat, aber sie geht nirgends hin. Die nächste Nachricht meldet SUBSCRIPTION_EXPIRED. Eine akzeptierte Sendung bedeutet, dass die Nachricht den Push-Dienst erreicht hat, nicht, dass der Benutzer sie gesehen hat.

Browserunterstützung

Alle wichtigen Desktop- und Mobilbrowser zeigen Push-Benachrichtigungen nach dem Abonnieren an. Beachten Sie diese Einschränkungen:

  • Auf iPhone und iPad funktioniert Push nur für Web-Apps, die ab iOS 16.4 zum Startbildschirm hinzugefügt wurden. In einem Safari-Tab meldet subscribe() UNSUPPORTED. Siehe Installierbare Apps für das erforderliche App-Manifest.
  • Safari zeigt keine Benachrichtigungs-Schaltflächen an. Es zeigt Nachrichten mit Aktionen ohne ihre Schaltflächen an, aber das Klicken auf die Benachrichtigung öffnet immer noch die Nachrichten-URL.
  • Android- und iOS-Webansichten zeigen keine Benachrichtigungen an.

Für die Details pro Browser siehe die MDN Kompatibilitätstabelle für showNotification.

Vollständiges Beispiel

Die folgende Ansicht abonniert und meldet den Browser ab, speichert Abonnements im Speicher und sendet eine Nachricht an jedes gespeicherte Abonnement. Es kann sofort oder nach acht Sekunden senden, indem es Springs TaskScheduler verwendet, sodass der Tab geschlossen werden kann, bevor die Benachrichtigung ankommt. Die App-Klasse verwendet @EnableScheduling, um den Scheduler verfügbar zu machen.

PushSubscriptions.java
package com.example;

import com.webforj.push.PushSubscription;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Service;

@Service
public class PushSubscriptions {

private final Map<String, PushSubscription> byEndpoint = new ConcurrentHashMap<>();

public void save(PushSubscription subscription) {
byEndpoint.put(subscription.getEndpoint(), subscription);
}

public void delete(PushSubscription subscription) {
byEndpoint.remove(subscription.getEndpoint());
}

public Collection<PushSubscription> findAll() {
return byEndpoint.values();
}
}
PushView.java
subscribe.onClick(ev -> Push.getCurrent().subscribe()
.thenAccept(subscription -> {
subscriptions.save(subscription);
status.setText("Abonniert");
})
.exceptionally(throwable -> {
WebforjPushException error = (WebforjPushException) throwable.getCause();
status.setText(error.getStatus() == PushStatus.PERMISSION_DENIED
? "Benachrichtigungen sind in diesem Browser blockiert"
: error.getMessage());

return null;
}));

unsubscribe.onClick(ev -> Push.getCurrent().unsubscribe().thenAccept(removed -> {
removed.ifPresent(subscriptions::delete);
status.setText(removed.isPresent() ? "Abgemeldet" : "Es gab kein Abonnement");
}));

sendNow.onClick(ev -> sendToAll(subscriptions, sender, message.getValue(), status::setText));

sendLater.onClick(ev -> {
String text = message.getValue();
status.setText("Senden in 8 Sekunden, schließen Sie jetzt den Tab");
scheduler.schedule(() -> sendToAll(subscriptions, sender, text, outcome -> {
}), Instant.now().plusSeconds(8));
});

Push.getCurrent().getSubscription().thenAccept(existing -> {
existing.ifPresent(subscriptions::save);
status.setText(existing.isPresent() ? "Abonniert" : "Nicht abonniert");
});

self.add(status, message, subscribe, unsubscribe, sendNow, sendLater);