Веб-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 Listsubscriptions = 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”