Веб-push-уведомления – это способ информировать пользователей вашего приложения о том, что произошло что-то важное.
Пользователи могут получать веб-push-уведомления, даже если они не активно используют ваше приложение, например, если приложение открыто на фоновой вкладке или даже если оно не открыто.
Push-уведомления широко поддерживаются всеми браузерами, кроме Safari: 78% веб-пользователей используют браузер, который их поддерживает.
В этом уроке я покажу вам как подписаться на уведомления в браузере и как отправлять уведомления с Java-сервера .
Видеоверсия
Немного предыстории: как работают веб-push-уведомления
Веб-push-уведомления основаны на двух веб-стандартах: API уведомлений и Push API (который, в свою очередь, использует Service Worker ). Для их работы требуется протокол HTTPS.
Подписка на push-уведомления
- Сервер делится своим открытым ключом с браузером
- Браузер использует открытый ключ для подписки на push-сервис (у каждого браузера свой собственный).
- Служба push возвращает подписку с уникальным URL-адресом конечной точки, который можно использовать для отправки push-сообщений
- Подписка сохраняется на сервере
Отправка push-уведомлений
- Сервер подписывает заголовок авторизации своим закрытым ключом
- Сервер отправляет сообщение на уникальный URL-адрес конечной точки
- Push-сервер расшифровывает заголовок авторизации
- Push-сервер отправляет сообщение на устройство/браузер
Настройте проект и сгенерируйте ДЕЙСТВИТЕЛЬНЫЕ ключи
Я использую Vaadin Fusion для этого примера. Использование термоядерного синтеза Пружинный ботинок на задней панели и Горит на передней панели.
Здесь я расскажу только о ключевых шагах. Вы можете найти полный исходный код на ГитХаб .
Вы можете создать новый проект Fusion с помощью командной строки Vaadin:
npx @vaadin/cli init --fusion push-app
Создайте набор ПУСТЫХ ключей с помощью пакета web-push npm.
npx web-push generate-vapid-keys
Создайте новый файл .environment в каталоге проекта и используйте его для хранения ключей. Добавьте его в свой файл .gitignore чтобы вы случайно не опубликовали его.
export VAPID_PUBLIC_KEY=BAwZxXp0K.... export VAPID_PRIVATE_KEY=1HLNMKEE....
Добавьте зависимость библиотеки Java Web Push в pom.xml :
nl.martijndwars web-push 5.1.1
Загрузите файл среды и запустите приложение:
source .env mvn
Создайте службу Java для обработки подписок и отправки уведомлений
Создайте новую службу загрузки Spring, MessageService.java . Эта служба будет считывать в ключах и
package com.example.application;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.Security;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.jose4j.lang.JoseException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import nl.martijndwars.webpush.Notification;
import nl.martijndwars.webpush.PushService;
import nl.martijndwars.webpush.Subscription;
@Service
public class MessageService {
@Value("${vapid.public.key}")
private String publicKey;
@Value("${vapid.private.key}")
private String privateKey;
private PushService pushService;
private List subscriptions = new ArrayList<>();
@PostConstruct
private void init() throws GeneralSecurityException {
Security.addProvider(new BouncyCastleProvider());
pushService = new PushService(publicKey, privateKey);
}
public String getPublicKey() {
return publicKey;
}
public void subscribe(Subscription subscription) {
System.out.println("Subscribed to " + subscription.endpoint);
this.subscriptions.add(subscription);
}
public void unsubscribe(String endpoint) {
System.out.println("Unsubscribing from " + endpoint);
subscriptions = subscriptions.stream().filter(s -> !endpoint.equals(s.endpoint)).collect(Collectors.toList());
}
public void sendNotification(Subscription subscription, String messageJson) {
try {
pushService.send(new Notification(subscription, messageJson));
} catch (GeneralSecurityException | IOException | JoseException | ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}
@Scheduled(fixedRate = 15000)
private void sendNotifications() {
System.out.println("Sending notifications to all subscribers");
var json = """
{
"title": "Server says hello!",
"body": "It is now: %s"
}
""";
subscriptions.forEach(subscription -> {
sendNotification(subscription, String.format(json, LocalTime.now()));
});
}
}
Некоторые ключевые моменты, на которые следует обратить внимание:
- Аннотация
@Value("${действительный.открытый.ключ}")считывает переменные среды в поля. - Служба хранит подписки в
Списке. В более практичном приложении вы бы сохранили их в базе данных вместе с пользователем. - Вы отправляете push-уведомления с помощью
push-сервиса.отправить(новое уведомление(подписка, Json сообщения)). Полезная нагрузка также может быть обычным текстом, но JSON является более гибким. - Сервис каждые 15 секунд рассылает всем абонентам уведомление, содержащее текущее время.
Создайте конечную точку для доступа к серверу
Далее вам нужен способ доступа к серверу из браузера. В Vaadin Fusion вы делаете это, определяя Конечную точку . Конечная точка будет генерировать типы машинописных текстов и методы доступа TS, которые вы можете использовать в клиентском коде.
package com.example.application;
import com.vaadin.flow.server.connect.Endpoint;
import com.vaadin.flow.server.connect.auth.AnonymousAllowed;
import nl.martijndwars.webpush.Subscription;
@Endpoint
@AnonymousAllowed
public class MessageEndpoint {
private MessageService messageService;
public MessageEndpoint(MessageService messageService) {
this.messageService = messageService;
}
public String getPublicKey() {
return messageService.getPublicKey();
}
public void subscribe(Subscription subscription) {
messageService.subscribe(subscription);
}
public void unsubscribe(String endpoint) {
messageService.unsubscribe(endpoint);
}
}
Некоторые вещи, которые следует отметить:
- Конечные точки защищены по умолчанию. Вы можете разрешить анонимный доступ с помощью
@AnonymousAllowed. - Конечная точка внедряет службу сообщений и делегирует ей подписку и отказ от подписки.
Подписаться на уведомления в браузере
Создайте представление для подписки на уведомления. Компонент Освещенный элемент отслеживает две части состояния:
- разрешил ли пользователь уведомления
- есть ли у пользователя существующая push-подписка
import { customElement, html, state } from "lit-element";
import "@vaadin/vaadin-button";
import { View } from "../view";
import * as server from "Frontend/generated/MessageEndpoint";
@customElement("notifications-view")
export class NotificationsView extends View {
@state()
denied = Notification.permission === "denied";
@state()
subscribed = false;
render() {
return html`
Web Push Notifications 📣
${this.denied
? html`
You have blocked notifications. You need to manually enable them
in your browser.
`
: ""}
${this.subscribed
? html`
Hooray! You are subscribed to receive notifications 🙌
Unsubscribe
`
: html`
You are not yet subscribed to receive notifications.
Subscribe
`}
`;
}
async firstUpdated() {
const registration = await navigator.serviceWorker.getRegistration();
this.subscribed = !!(await registration?.pushManager.getSubscription());
}
async subscribe() {
const notificationPermission = await Notification.requestPermission();
if (notificationPermission === "granted") {
const publicKey = await server.getPublicKey();
const registration = await navigator.serviceWorker.getRegistration();
const subscription = await registration?.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: this.urlB64ToUint8Array(publicKey),
});
if (subscription) {
this.subscribed = true;
// Serialize keys uint8array -> base64
server.subscribe(JSON.parse(JSON.stringify(subscription)));
}
} else {
this.denied = true;
}
}
async unsubscribe() {
const registration = await navigator.serviceWorker.getRegistration();
const subscription = await registration?.pushManager.getSubscription();
if (subscription) {
await subscription.unsubscribe();
await server.unsubscribe(subscription.endpoint);
this.subscribed = false;
}
}
private urlB64ToUint8Array(base64String: string) {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding)
.replace(/\-/g, "+")
.replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
}
Важной частью здесь является метод subscribe() -. Вот что он делает:
- Запрашивает у пользователя разрешение на показ уведомлений с помощью
Notification.requestPermission(). Ответ будет “удовлетворен” или “отклонен”. записка: Если пользователь откажется, вы не сможете спросить его снова. Обязательно запрашивайте пользователя только тогда, когда он ожидает и хочет получать уведомления. - Если пользователь дает разрешение, извлеките открытый ключ с сервера и используйте pushManager Service Worker для подписки на уведомления. Ключ
Сервера приложений– это массив UINT8, содержащий открытый ключ. Вам нужно преобразовать его с помощью прилагаемого метода. (Не самый удобный API 🤷 ♂ ️) - Если подписка завершится успешно, отправьте ее на сервер.
Обрабатывать входящие push-сообщения в Сервисном работнике
Как только вы подпишетесь на уведомления, сервер будет отправлять уведомления каждые 15 секунд.
Переопределите сотрудника службы, созданного Vaadin, скопировав цель/sw.ts -> интерфейс/sw.ts .
Добавьте следующих двух слушателей в sw.ts :
self.addEventListener("push", (e) => {
const data = e.data?.json();
if (data) {
self.registration.showNotification(data.title, {
body: data.body,
});
}
});
self.addEventListener("notificationclick", (e) => {
e.notification.close();
e.waitUntil(focusOrOpenWindow());
});
async function focusOrOpenWindow() {
const url = new URL("/", self.location.origin).href;
const allWindows = await self.clients.matchAll({
type: "window",
});
const appWindow = allWindows.find((w) => w.url === url);
if (appWindow) {
return appWindow.focus();
} else {
return self.clients.openWindow(url);
}
}
Прослушиватель
fetchвызывается при поступлении нового сообщения. Прочитайте свойство событияданныхкак JSON, чтобы получить доступ к полезной нагрузке сообщения.- Используйте
самостоятельную регистрацию.showNotification()для отображения уведомления с использованием данных сообщения.
- Используйте
Прослушиватель
щелчок уведомлениявызывается при нажатии на уведомление.- Закройте уведомление.
- Посмотрите, есть ли у пользователя открытая вкладка “Приложение”. Если они это сделают, сосредоточьтесь на этом. Если они этого не сделают, откройте новое окно.
Исходный код
Вы можете найти полный исходный код на моем GitHub: https://github.com/marcushellberg/fusion-push-notifications .
Оригинал: “https://dev.to/marcushellberg/how-to-send-web-push-notifications-in-java-21lo”