Web Push Notifications (Laravel + Vue.js)
Bài đăng này đã không được cập nhật trong 3 năm
- Ở 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 app\Notifications\MessageNotification.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 NotificationChannels\WebPush\WebPushMessage; use NotificationChannels\WebPush\WebPushChannel; ... 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 \Illuminate\Notifications\Messages\DatabaseMessage */ 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 route\web.php để gửi notifications tới trình duyệt khi có tin nhắn mớiuse App\Events\MessagePosted; use App\Notifications\MessageNotification; 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' => [ ... NotificationChannels\WebPush\WebPushServiceProvider::class, ],
- Thêm trait
NotificationChannels\WebPush\HasPushSubscriptions
vào modelUser
use NotificationChannels\WebPush\HasPushSubscriptions; 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="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations" php artisan migrate
- Tạo file config\webpush.php
php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --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 App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Foundation\Validation\ValidatesRequests; class PushSubscriptionController extends Controller { use ValidatesRequests; /** * Create a new controller instance. * * @return void */ public function __construct() { $this->middleware('auth'); } /** * Update user's subscription. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function update(Request $request) { $this->validate($request, ['endpoint' => 'required']); $request->user()->updatePushSubscription( $request->endpoint, $request->key, $request->token ); } /** * Delete the specified subscription. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ 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 *route\web.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 resource\views\chat.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 resource\assets\js\app.jsVue.component('notifications-demo', require('./components/NotificationsDemo.vue'));
-
File resource\assets\js\components\NotificationDemo.vue có nội dung như sau:
<template> <div> <!-- Enable/Disable push notifications --> <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 resoures\views\layouts\app.blade.php
<!-- GCM Manifest (optional if VAPID is used) --> @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
Tài liệu tham khảo
All rights reserved