Web Push Notifications (Laravel + Vue.js)
Ở bài trước mình đã giới thiệu đến các bạn bài viết: Chat room với laravel 5.5 và Vue.js trong 15 phút. Sau bài viết bạn có thể tự tạo cho mình 1 chat room đơn giản, bài này sẽ là tiếp nối của bài trước, ta sẽ tạo ra các thông báo đẩy cho trên trình duyệt (web push notifications) để thông báo cho ...
- Ở bài trước mình đã giới thiệu đến các bạn bài viết: Chat room với laravel 5.5 và Vue.js trong 15 phút. Sau bài viết bạn có thể tự tạo cho mình 1 chat room đơn giản, bài này sẽ là tiếp nối của bài trước, ta sẽ tạo ra các thông báo đẩy cho trên trình duyệt (web push notifications) để thông báo cho bạn biết khi có người nhắn tin tới room của bạn, ngay cả khi bạn không mở trang. Cụ thể như facebook, chatwork hay zalo chẳng hạn, nếu bật notifications cho trình duyệt bạn sẽ nhận được những thông báo kiểu như:
I. Chuẩn bị
-
Tạo project chatdemo dựa theo hướng dẫn ở bài trước.
-
Tiếp theo ta cần tạo 1 project tại Google Console
-
Sau đó ta cần add project vào firebase tại đây và lưu server_key, Sender ID trong phần setting tab CLOD MESSAGING (link https://console.firebase.google.com/project/<your project_id>/settings/cloudmessaging) lại để dùng trong phần dưới
-
Bạn có thể test nhanh bằng key mẫu của mình đã tạo sẵn như sau:
PUSHER_APP_ID=446989 PUSHER_APP_KEY=a0cfcaccd70a193bc043 PUSHER_APP_SECRET=fcfe23bd8422cc0abed7 GCM_KEY=AAAAsmHKKiQ:APA91bEYEaQf-QXpevKq3jeXPcg2CyychJPa87k66tGuTdbs3HnqTuAbUmgMxneIG5pxeyyfEvAveFo7sKvXUtZ5vsc-BWeF_5mUrFKWvoCxcLBgnYcicfJFdk3ILtyOqvsK5Fze1F0C GCM_SENDER_ID=766144817700
II. Notifications
- Phần này bạn có thể tham khảo tại Document Notifications
- Tạo class PushNotification với câu lệnh:
php artisan make:notification MessageNotification
- Câu lệnh này sẽ tạo ra file appNotificationsMessageNotification.php, đây là file chứ nội dung thông báo ta gửi đến trình duyệt, ta cần thêm một vài đoạn code như sau:
use NotificationChannelsWebPushWebPushMessage; use NotificationChannelsWebPushWebPushChannel; ... public $message; public $user; public function via($notifiable) { return [WebPushChannel::class]; } /** * Get the web push representation of the notification. * * @param mixed $notifiable * @param mixed $notification * @return IlluminateNotificationsMessagesDatabaseMessage */ public function toWebPush($notifiable, $notification) { return (new WebPushMessage) ->title($this->user->name . ' đã nhắn tin đến nhóm của bạn.') ->icon('/avatar.png') ->body($this->message->message); }
- Class WebPushMessage và WebPushChannel sẽ được tạo ở phần III. Webpush package dưới đây.
- Tiếp đến ta cần update messages trong routeweb.php để gửi notifications tới trình duyệt khi có tin nhắn mới
use AppEventsMessagePosted; use AppNotificationsMessageNotification; Route::post('/messages', function () { $user = Auth::user(); $userIdJoined = Message::where('user_id', '!=', $user->id)->groupBy('user_id')->pluck('user_id'); $userJoined = User::whereIn('id', $userIdJoined)->get(); $message = $user->messages()->create([ 'message' => request()->get('message') ]); Notification::send($userJoined, new MessageNotification($message, $user)); broadcast(new MessagePosted($message, $user))->toOthers(); return ['status' => 'OK']; })->middleware('auth');
III. WebPush package
- Tại thư mục gốc của project chatdemo ta cài đặt package webpush với câu lệnh:
composer require laravel-notification-channels/webpush
- Thêm service provider vào config/app.php:
// config/app.php 'providers' => [ ... NotificationChannelsWebPushWebPushServiceProvider::class, ],
- Thêm trait NotificationChannelsWebPushHasPushSubscriptions vào model User
use NotificationChannelsWebPushHasPushSubscriptions; class User extends Model { use HasPushSubscriptions; }
- Tạo bảng push_subscriptions với 2 câu lệnh sau:
php artisan vendor:publish --provider="NotificationChannelsWebPushWebPushServiceProvider" --tag="migrations" php artisan migrate
- Tạo file configwebpush.php
php artisan vendor:publish --provider="NotificationChannelsWebPushWebPushServiceProvider" --tag="config"
- Tạo VAPID key với câu lệnh:
php artisan webpush:vapid
- Lệnh này sẽ set VAPID_PUBLIC_KEY và VAPID_PRIVATE_KEY trong file .env cho bạn.
- Bạn nhớ thêm key của bạn ở phần I. Chuẩn bị vào .env nhé
IV. Tạo nút Enable/Disable Notification
-
Tạo controller PushSubscriptionController
namespace AppHttpControllers; use IlluminateHttpRequest; use IlluminateRoutingController; use IlluminateFoundationValidationValidatesRequests; class PushSubscriptionController extends Controller { use ValidatesRequests; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } /** * Update user's subscription. * * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse */ public function update(Request $request) { $this->validate($request, ['endpoint' => 'required']); $request->user()->updatePushSubscription( $request->endpoint, $request->key, $request->token ); } /** * Delete the specified subscription. * * @param IlluminateHttpRequest $request * @return IlluminateHttpResponse */ public function destroy(Request $request) { $this->validate($request, ['endpoint' => 'required']); $request->user()->deletePushSubscription($request->endpoint); return response()->json(null, 204); } }
-
Thêm route vào *routeweb.php
// Push Subscriptions Route::post('subscriptions', 'PushSubscriptionController@update'); Route::post('subscriptions/delete', 'PushSubscriptionController@destroy'); // Manifest file (optional if VAPID is used) Route::get('manifest.json', function () { return [ 'name' => config('app.name'), 'gcm_sender_id' => config('webpush.gcm.sender_id') ]; });
-
Thêm view vào resourceviewschat.blade.php
<div class="row"> <div class="col-md-8 col-md-offset-2"> <div class="panel panel-default"> <div class="panel-heading">Dashboard</div> <div class="panel-body text-center"> {{-- See resources/assets/js/components/NotificationsDemo.vue --}} <notifications-demo></notifications-demo> </div> </div> </div> </div>
-
Thêm NotificationsDemo vue component vào resourceassetsjsapp.js
Vue.component('notifications-demo', require('./components/NotificationsDemo.vue'));
-
File resourceassetsjscomponentsNotificationDemo.vue có nội dung như sau:
<template> <div> <button @click="togglePush" :disabled="pushButtonDisabled || loading" type="button" class="btn btn-primary" :class="{ 'btn-primary': !isPushEnabled, 'btn-danger': isPushEnabled }"> {{ isPushEnabled ? 'Disable' : 'Enable' }} Push Notifications </button> </div> </template> <script> import axios from 'axios' export default { data: () => ({ loading: false, isPushEnabled: false, pushButtonDisabled: true }), mounted () { this.registerServiceWorker() }, methods: { /** * Register the service worker. */ registerServiceWorker () { if (!('serviceWorker' in navigator)) { console.log('Service workers aren't supported in this browser.') return } navigator.serviceWorker.register('/sw.js') .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 = window.Laravel.vapidPublicKey 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 } }) }) }, /** * Unsubscribe from push notifications. */ unsubscribe () { navigator.serviceWorker.ready.then(registration => { registration.pushManager.getSubscription().then(subscription => { if (!subscription) { this.isPushEnabled = false this.pushButtonDisabled = false return } subscription.unsubscribe().then(() => { this.deleteSubscription(subscription) this.isPushEnabled = false this.pushButtonDisabled = false }).catch(e => { console.log('Unsubscription error: ', e) this.pushButtonDisabled = false }) }).catch(e => { console.log('Error thrown while unsubscribing.', e) }) }) }, /** * Toggle push notifications subscription. */ togglePush () { if (this.isPushEnabled) { this.unsubscribe() } else { this.subscribe() } }, /** * 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 data = { endpoint: subscription.endpoint, key: key ? btoa(String.fromCharCode.apply(null, new Uint8Array(key))) : null, token: token ? btoa(String.fromCharCode.apply(null, new Uint8Array(token))) : null } this.loading = true axios.post('/subscriptions', data) .then(() => { this.loading = false }) }, /** * Send a requst to the server to delete user's subscription. * * @param {PushSubscription} subscription */ deleteSubscription (subscription) { this.loading = true axios.post('/subscriptions/delete', { endpoint: subscription.endpoint }) .then(() => { this.loading = false }) }, /** * 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 } } } </script> <style scoped> .btn { margin-right: 10px; margin-bottom: 10px; } </style>
-
Cuối cùng ta sẽ thêm đoạn script sao vào phần head trong file resouresviewslayoutsapp.blade.php
@if (config('webpush.gcm.sender_id')) <link rel="manifest" href="/manifest.json"> @endif <script> window.Laravel = {!! json_encode([ 'user' => Auth::user(), 'csrfToken' => csrf_token(), 'vapidPublicKey' => config('webpush.vapid.public_key'), 'pusher' => [ 'key' => config('broadcasting.connections.pusher.key'), 'cluster' => config('broadcasting.connections.pusher.options.cluster'), ], ]) !!}; </script>
-
Và kết quả mình thu được:
-
Hy vọng bài viết sẽ giúp ích được bạn, nếu bạn có gặp khó khăn gì trong lúc thực hiện hãy liên hệ với mình hoặc tài liệu tham khảo bên dưới