12/08/2018, 16:54

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

0