Web Push Benachrichtigungen in Laravel und Vue.JS Anwendungen

In diesem Blog-Beitrag zeigen wir, wie man Web-Push-Benachrichtigungen zu einem Laravel-Backend mit einem Vue.js-Frontend hinzufügen kann. Web-Push-Benachrichtigungen ermöglichen es, Benachrichtigungen an Benutzer zu senden, auch wenn diese die Website nicht aktiv nutzen. Dies kann in einer Vielzahl von Situationen nützlich sein, zum Beispiel wenn Benutzer über neue Tickets in einem Ticketsystem benachrichtigt werden sollen, wie wir es in diesem Beispiel tun werden.

Backend

Wir benutzen hier einen Teil von laravel-notification-channels, eine Sammlung von Projekten aus denen wir schon hier berichtet haben. Wir beginnen mit der Installation des Pakets über composer, indem wir den Befehl ausführen:

composer require laravel-notification-channels/webpush

Dieser Befehl installiert das Paket, mit dem Web-Push-Benachrichtigungen gesendet werden können.

Als nächstes fügen wir die use-Anweisung zu unserem User-Modell hinzu:

use NotificationChannels\WebPush\HasPushSubscriptions;

Diese Codezeile ermöglicht es, die vom Paket bereitgestellten Funktionen im User-Modell verwenden zu können:

class User extends Model { 
    use HasPushSubscriptions;
    // ....
}

Anschließend führen wir den Befehl aus:

php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations"

Dieser Befehl wird verwendet, um die Migration zu veröffentlichen, die notwendig ist, um die erforderliche Tabelle in der Datenbank zu erstellen, in der die Push-Abonnements gespeichert werden. Die Migration erstellt eine Tabelle, in der alle Push-Abonnements gespeichert werden, so dass sie später zum Senden von Benachrichtigungen verwendet werden kann.

Wenn gewünscht, kann die Konfigurationsdatei auch mit veröffentlicht werden:

php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="config"

Um die VAPID-Schlüssel zu generieren, führen wir den folgenden Befehl aus:

php artisan webpush:vapid

Mit diesem Befehl werden VAPID_PUBLIC_KEY und VAPID_PRIVATE_KEY in der .env-Datei festgelegt.

VAPID-Schlüssel sind eine Methode zur Identifizierung des Absenders von Web-Push-Benachrichtigungen. Sie bestehen aus zwei Schlüsseln, einem öffentlichen Schlüssel und einem privaten Schlüssel, wobei der öffentliche Schlüssel zur Verschlüsselung der Nutzdaten der Push-Benachrichtigung und der private Schlüssel zur Signierung der Push-Benachrichtigung verwendet wird. Sie werden verwendet, um eine sichere Verbindung zwischen dem Anwendungsserver und dem Push-Dienst herzustellen und um sicherzustellen, dass nur autorisierte Parteien Push-Benachrichtigungen an den Browser eines Benutzers senden können.

Anschließend erstellen wir eine neue Benachrichtigung, indem wir den Befehl ausführen:

php artisan make:notification NewTicketNotification

Dieser Befehl erstellt eine neue Benachrichtigungsklasse, die wir verwenden können, um eine Benachrichtigung zu senden, wenn ein neues Ticket erstellt oder bearbeitet wird.

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use NotificationChannels\WebPush\WebPushMessage;
use NotificationChannels\WebPush\WebPushChannel;

class NewTicketNotification extends Notification
{
    use Queueable;

    public $data;

    /**
     * Create a new notification instance.
     * @param array $data The data for the notification.
     * @return void
     */
    public function __construct($data)
    {
        $this->data = $data;
    }

    /**
     * Get the notification's delivery channels.
     *
     * @param mixed $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return [WebPushChannel::class];
    }


    /**

     * This function is used to send web push notifications to the user.
     *
     * @param mixed $notifiable
     * @param \Illuminate\Notifications\Notification $notification
     * @return \Minishlink\WebPush\Message
     */
    public function toWebPush($notifiable, $notification)
    {
        return (new WebPushMessage)
            ->title($this->data['title'])
            ->icon('/images/cube.svg')
            ->body($this->data['body'])
            ->action('Jetzt anzeigen', '/#/task/' . $this->data['id'])
            ->options(
                ['TTL' => 86400],
            )
            ->requireInteraction()
            ->vibrate(2000);
    }
}

In unserem Ticket-Controller können wir die Benachrichtigung direkt nach der Erstellung oder Bearbeitung des Tickets ausführen:

use App\Notifications\NewTicketNotification;

// ...

if ($task->user_id != Auth::user()->id) { 
    // create data with our information
    $data['title'] = 'Neues Ticket von ' . Auth::user()->first_name;
    $data['body'] = $task->customer->name . ': ' . $task->name;
    $data['id'] = $task->id; 
    // send notification
    $task->user->notify(new NewTicketNotification($data)); 
}

Dieser Code prüft, ob der Benutzer der das Ticket erstellt hat, nicht derselbe ist wie der Benutzer, dem das Ticket zugewiesen ist. Wenn dies der Fall ist, wird eine Benachrichtigung mit den erforderlichen Informationen, wie Titel, Text und ID des Tickets erstellt und der zugewiesene Benutzer benachrichtigt.

Frontend

Für die Benachrichtigung des Benutzers wird ein Service Worker benötigt. Ein Service-Worker ist ein Skript, das im Hintergrund einer Webseite läuft, getrennt von der Webseite selbst. Es fungiert als Proxy zwischen der Webseite und dem Netzwerk und kann für Aufgaben wie Offline-Support, Hintergrundsynchronisierung, Push-Benachrichtigungen usw. verwendet werden.

// /public/sw.js
(() => {
    'use strict';

    const WebPush = {
        init() {
            self.addEventListener('push', this.notificationPush.bind(this));
            self.addEventListener('notificationclick', this.notificationClick.bind(this));
        },

        /**
         * Handle notification push event.
         * https://developer.mozilla.org/en-US/docs/Web/Events/push
         * @param {NotificationEvent} event
         */
        notificationPush(event) {
            if (!(self.Notification && self.Notification.permission === 'granted')) {
                return;
            }

            // https://developer.mozilla.org/en-US/docs/Web/API/PushMessageData
            if (event.data) {
                event.waitUntil(this.sendNotification(event.data.json()));
            }
        },

        /**
         * Handle notification click event.
         * https://developer.mozilla.org/en-US/docs/Web/Events/notificationclick
         * @param {NotificationEvent} event
         */
        notificationClick(event) {
            // console.log(event.notification)

            if (event.action.startsWith('/#')) {
                self.clients.openWindow(event.action);
            } else {
                self.clients.openWindow('/#/dashboard');
            }
        },

        /**
         * Send notification to the user.
         * https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration/showNotification
         * @param {PushMessageData|Object} data
         */
        sendNotification(data) {
            return self.registration.showNotification(data.title, data);
        },
    };

    WebPush.init();
})();

Um einen Service Worker zu verwenden, muss dieser zunächst auf der Webseite registriert werden. Dies geschieht in der Regel in der Haupt-JavaScript-Datei der Webseite mit der Methode navigator.serviceWorker.register(). Sobald der Service Worker registriert ist, wird er heruntergeladen und im Hintergrund ausgeführt.

Wenn ein Service Worker eine Push-Benachrichtigung erhält, kann er dem Benutzer eine Benachrichtigung anzeigen, auch wenn die Webseite geschlossen ist. Dies ist möglich, weil Service Worker auf einem separaten Thread laufen und Zugang zu einer speziellen API haben, die es ihnen erlaubt, Benachrichtigungen anzuzeigen.

Der Benutzer wird aufgefordert, die Push-Benachrichtigungen zu akzeptieren oder zu blockieren wenn er die Website besucht. Diese Aufforderung ist eine Browserfunktion und wird vom Browser angezeigt wenn die Website die Methode Notification.requestPermission() aufruft. Der Benutzer kann dann entscheiden, ob er die Benachrichtigungen für die Website zulassen oder blockieren möchte.

In unserer Haupt Vue.JS Datei haben wir den folgenden Code hinzugefügt damit der Service Worker registriert wird:

mounted() {
	this.subscribe();
	this.registerServiceWorker();
},

methods: {
	registerServiceWorker() {
		if (!('serviceWorker' in navigator)) {
			console.log("Service workers aren't supported in this browser.");
			return;
		}
		navigator.serviceWorker.register('/sw.js?timestamp=' + Date.now()).then(() => {
			this.initialiseServiceWorker();
		});
	},

	initialiseServiceWorker() {
		if (!('showNotification' in ServiceWorkerRegistration.prototype)) {
			console.log("Notifications aren't supported.");
			return;
		}
		if (Notification.permission === 'denied') {
			console.log('The user has blocked notifications.');
			return;
		}
		if (!('PushManager' in window)) {
			console.log("Push messaging isn't supported.");
			return;
		}
		navigator.serviceWorker.ready.then((registration) => {
			registration.pushManager
				.getSubscription()
				.then((subscription) => {
					this.pushButtonDisabled = false;
					if (!subscription) {
						return;
					}
					this.updateSubscription(subscription);
					this.isPushEnabled = true;
				})
				.catch((e) => {
					console.log('Error during getSubscription()', e);
				});
		});
	},

	/**
	 * Subscribe for push notifications.
	 */
	subscribe() {
		navigator.serviceWorker.ready.then((registration) => {
			const options = { userVisibleOnly: true };
			const vapidPublicKey = 'vapidPublicKey from your .env file goes here';
			if (vapidPublicKey) {
				options.applicationServerKey = this.urlBase64ToUint8Array(vapidPublicKey);
			}
			registration.pushManager
				.subscribe(options)
				.then((subscription) => {
					this.isPushEnabled = true;
					this.pushButtonDisabled = false;
					this.updateSubscription(subscription);
				})
				.catch((e) => {
					if (Notification.permission === 'denied') {
						console.log('Permission for Notifications was denied');
						this.pushButtonDisabled = true;
					} else {
						console.log('Unable to subscribe to push.', e);
						this.pushButtonDisabled = false;
					}
				});
		});
	},

	/**
	 * Send a request to the server to update user's subscription.
	 *
	 * @param {PushSubscription} subscription
	 */
	updateSubscription(subscription) {
		const key = subscription.getKey('p256dh');
		const token = subscription.getKey('auth');
		const contentEncoding = (PushManager.supportedContentEncodings || ['aesgcm'])[0];

		const data = {
			endpoint: subscription.endpoint,
			publicKey: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null,
			authToken: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null,
			contentEncoding,
		};

		axios.post('/api/subscriptions', data).then(() => {
			// this.showSnackbar('Aktiviert', 'green');
		});
	},

	/**
	 * https://github.com/Minishlink/physbook/blob/02a0d5d7ca0d5d2cc6d308a3a9b81244c63b3f14/app/Resources/public/js/app.js#L177
	 *
	 * @param  {String} base64String
	 * @return {Uint8Array}
	 */
	urlBase64ToUint8Array(base64String) {
		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;
	},
}

Loggt sich der Benutzer jetzt ein wird er sofort gefragt ob Benachrichtigungen aktiviert werden sollen. Sobald diese aktiviert sind, können Benachrichtigungen sofort empfangen werden.

Als Basis für diesen Blogpost wurde die Github Seite von laravel-notification-channels/webpush und cretueusebiu/laravel-web-push-demo verwendet.

Sollten Sie noch Fragen haben oder eine Beratung wünschen, können Sie gerne mit uns Kontakt aufnehmen oder unsere Webseite besuchen.

Gerne können Sie hier auch andere Artikel zum Thema Laravel anschauen.