九保すこひ@フリーランスエンジニア|累計300万PVのブログ運営中
さてさて、この間のコピペでOK!Laravelからwordpressに投稿する機能という記事ではLaravelに独自のNotification
を作ってwordpress
に投稿する機能を実装しました。
そして、これに関連してNotification
で他に何かできないかと考えていたところ、ちょっと手間が大きいので「また今度にしよう😉」と考えていた機能を思い出しました。
それが、
プッシュ通知(WebPush)
です。
プッシュ通知とは、ある特定の通知がリアルタイムでユーザーにお知らせされる通知のことで、WebPush
はなんとブラウザでサイトを開いていなくても通知させることができるようになります。
そして実は、最近のブラウザにはプッシュ通知機能が実装されているのを知っているでしょうか。(ブラウザのサポート状況はCan I useをご覧ください)
そこで!
今回はブラウザにプッシュ通知を送るWebPushを我らがLaravelで実装してみたいと思います。
ぜひ皆さんのお役に立てると嬉しいです😊✨
開発環境: Laravel 6.x、Google Chrome 79
目次
やりたいこと
今回はプッシュ通知を使って、「ユーザーの皆さん、新しいイベントが登録されましたよ!クリックしてイベントのページを見に来てね😊✨」というお知らせができるようにしてみたいと思います。
では、やっていきましょう!
前提として
今回の記事は、Laravel
のログイン機能がすでにインストールされていることを前提としています。
そのため、もしインストールがまだの方は以下を参考にして準備しておいてください。
- Laravel 6.x 以上 ・・・ Laravel6.0でログイン機能を使う方法
- それ未満 ・・・ 【Laravel5.6】インストール直後にやること3点
また、プッシュ通知はローカル環境であってもHTTPS
での接続が必須となっています。
※ 厳密には自己証明書でのHTTPS
も不可です。対処法については接続環境のエラーについてをご覧ください。
パッケージをインストールする
WebPush
専用のパッケージが公開されていますので、先にcomposer
でインストールしておきます。
composer require laravel-notification-channels/webpush
準備する
DBにテーブルをつくる
先ほどインストールしたパッケージからマイグレーションをアプリケーション側にコピーします。以下のコマンドを実行してください。
php artisan vendor:publish --provider="NotificationChannels\WebPush\WebPushServiceProvider" --tag="migrations"
すると、database/migrations/****_**_**_******_create_push_subscriptions_table.php
というファイルが作成されるので以下のコマンドを実行してDBに専用テーブルを作成しましょう。
php artisan migrate
実行が完了するとテーブルは次のようになります。
トレイトをモデルに追加する
WebPush
が使えるようにapp/User.php
へHasPushSubscriptions
という名前のトレイトを追加します。
<?php namespace App; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use NotificationChannels\WebPush\HasPushSubscriptions; class User extends Authenticatable { use Notifiable; use HasPushSubscriptions; // 以下省略
VAPIDをつくる
WebPush
を実装するために必要なキーをつくります。
以下のコマンドを実行してください。
php artisan webpush:vapid
すると、自動的に.env
内に必要なキーを追加してくれます。
VAPID_PUBLIC_KEY=****************************** APID_PRIVATE_KEY=******************************
プッシュ通知のNotificationをつくる
では、Laravel
からWebPush
が使えるように専用のNotification
をつくりましょう。以下のコマンドを実行してください。
php artisan make:notification EventAdded
すると、app/Notifications/EventAdded.php
というファイルが作成されるので、中身を以下のように変更します。
<?php namespace App\Notifications; use Illuminate\Notifications\Notification; use NotificationChannels\WebPush\WebPushChannel; use NotificationChannels\WebPush\WebPushMessage; class EventAdded extends Notification { public function via($notifiable) { return [WebPushChannel::class]; } public function toWebPush($notifiable, $notification) { return (new WebPushMessage) ->title('新イベント') ->body('新しいイベントが追加されました!') ->data([ 'url' => url('/test/15') ]); } }
この中では、先ほどインストールしたパッケージのWebPushMessage
を使ってメッセージを送信することになります。今回はシンプルに4つだけパラメータを指定していますが、その他のオプションはこちらのページを参考にしてください。
また、data()
内のurl
はテストのURL
です。
本番環境で実装するには、該当するURL
に置き換えてください。
プッシュ通知の登録ページをつくる
ではここからは、ユーザーがプッシュ通知を許可し、そのデータがサイト側に保存されるまでの作業をしていきます。
ルートをつくる
まずはルートをつくります。
routes/web.php
を開いて以下の2行を追加してください。
Route::get('web_push/create', 'WebPushController@create'); Route::post('web_push', 'WebPushController@store');
コントローラーをつくる
次にコントローラーをつくります。
以下のコマンドを実行してください。
php artisan make:controller WebPushController
すると、app/Http/Controllers/WebPushController.php
というファイルが作成されるので、中身を以下のように変更してください。
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class WebPushController extends Controller { public function __construct() { $this->middleware('auth'); // 要ログイン } public function create() { return view('web_push.create'); } public function store(Request $request) { $this->validate($request, [ 'endpoint' => 'required', 'keys.auth' => 'required', 'keys.p256dh' => 'required' ]); $endpoint = $request->endpoint; $token = $request->keys['auth']; $key = $request->keys['p256dh']; $user = $request->user(); $user->updatePushSubscription($endpoint, $key, $token); return response()->json([ 'success' => true ], 200); } }
なお、この中で重要なのは$this->middleware('auth');
の部分です。
意味としては「このコントローラーにアクセスする場合、ログインが必須」となりますが、これはstore()
の中で特定のユーザーを取得する必要があるためです。
ビューをつくる
続いて、プッシュ通知を登録するページのビュー(HTML+JavaScript)を作っていきましょう。
resources/views/web_push/create.blade.php
というファイルを作って中身を以下のようにしてください。
<html> <body> <div id="app"> <div v-if="processing">処理中...</div> <div v-else> <button type="button" @click="subscribe" v-if="!isSubscribed">イベントのプッシュ通知を登録する</button> <button type="button" @click="unsubscribe" v-else>イベントのプッシュ通知を解除する</button> </div> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script> <script> new Vue({ el: '#app', data: { vapidPublicKey: '{{ config('webpush.vapid.public_key') }}', registration: null, isSubscribed: false, processing: false, csrfToken: '{{ csrf_token() }}' }, methods: { subscribe() { // プッシュ通知を許可する this.processing = true; const applicationServerKey = this.base64toUint8(this.vapidPublicKey); const options = { userVisibleOnly: true, applicationServerKey: applicationServerKey }; this.registration.pushManager.subscribe(options) .then(subscription => { // Laravel側へデータを送信 fetch('/web_push', { method: 'POST', body: JSON.stringify(subscription), headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-CSRF-Token': this.csrfToken } }) .then(response => { this.isSubscribed = true; alert('プッシュ通知が登録されました'); }) .catch(error => { console.log(error); }); }) .finally(() => { this.processing = false; }); }, unsubscribe() { // プッシュ通知を解除する this.processing = true; this.registration.pushManager.getSubscription() .then(subscription => { subscription.unsubscribe() .then(result => { if(result) { this.isSubscribed = false; alert('プッシュ通知が解除されました'); } }); }) .finally(() => { this.processing = false; }); }, base64toUint8(str) { str += '='.repeat((4 - str.length % 4) % 4); const base64 = str .replace(new RegExp('\-', 'g'), '+') .replace(new RegExp('_', 'g'), '/'); const binary = window.atob(base64); const binaryLength = binary.length; let uint8Array = new Uint8Array(binaryLength); for(let i = 0; i < binaryLength; i++) { uint8Array[i] = binary.charCodeAt(i); } return uint8Array.buffer; } }, mounted() { if('serviceWorker' in navigator && 'PushManager' in window) { // Service Workerをブラウザにインストールする navigator.serviceWorker.register('/sw.js') .then(registration => { console.log('Service Worker が登録されました。'); this.registration = registration; this.registration.pushManager.getSubscription() .then(subscription => { this.isSubscribed = !(subscription === null); }); }); } else { console.log('このブラウザは、プッシュ通知をサポートしていません。'); } } }); </script> </body> </html>
※ テストのため、エラー処理などは省略していますので気をつけてください。
この中で重要なのは以下の3つの部分です。
- Service Workerを登録する
- プッシュ通知を許可する
- プッシュ通知を解除する
少しコードが長いのでそれぞれ説明していきます。
Service Workerを登録する
プッシュ通知を実装するには、まずService Worker
と呼ばれるJavaScript
をブラウザに登録(インストール)しておいて、プッシュ通知されたり、プッシュ通知がクリックされたときにこのコードが実行されることになります。(Service Worker
の本体コードは次の項目で作成します)
そのため、ページが表示された時点で呼ばれるmounted()
の中で実行しています。
プッシュ通知を許可する
「イベントのプッシュ通知を登録する」ボタンがクリックされたときに呼ばれるsubscribe()
メソッド内でプッシュ通知が登録されることになります。
この中では、pushManager.subscribe()
を実行&許可を得ることで、登録情報を取得し、これをLaravel
側に 送信することになります。
なお、送信先は、WebPushController
のstore()
です。
プッシュ通知を解除する
すでにプッシュ通知が許可されている場合に表示される「イベントのプッシュ通知を解除する」ボタンがクリックされたときに実行されるのが、unsubscribe()
メソッドです。
Service Worker本体をつくる
先ほどの項目でService Worker
を登録するコードを書きましたが、まだService Worker
本体のファイルを作成していませんので、ここでつくりましょう。
public/sw.js
にファイルを作成し、中身を以下のようにしてください。
self.addEventListener('push', e => { // プッシュ通知された時 const json = e.data.json(); const title = json.title; const options = { body: json.body, data: { url: json.data.url, } }; e.waitUntil( self.registration.showNotification(title, options) ); }); self.addEventListener('notificationclick', e => { // 通知がクリックされた時 const data = e.notification.data; e.waitUntil( clients.openWindow(data.url) ); });
テストしてみる
では、実際にテストしてみましょう。
※ 注意:もし実行してエラーが表示された場合は、接続環境のエラーについてを参考にしてください。(というか、おそらくエラー表示されると思います)
まずhttps://*****/web_push/create
にアクセスしてください。
すると、以下のような表示になりますのでボタンをクリックしてプッシュ通知に許可してください。
許可が完了すると、次のようなアラートが表示されます。
では、この状態で以下のようにテストコードをroutes/web.php
に追加して、https://******/web_push_test
にアクセスをしてみましょう。
// 全ユーザーにプッシュ通知を試みる Route::get('web_push_test', function(){ $users = \App\User::all(); \Notification::send($users, new \App\Notifications\EventAdded()); });
すると、次のような通知が来ました!(OS
がLinux
なので、Windows
ではまた違ったデザインだと思います)
うまくいきました😊✨
エラーが表示されたら
接続環境のエラーについて
おそらくですが、JavaScript
のコードを実行しようとすると、ブラウザのDevTools
に以下のようなエラーが表示されると思います。
DOMException: Failed to register a ServiceWorker for scope ('https://******/') with script ('https://******/sw.js'): An SSL certificate error occurred when fetching the script.
なぜこの表示が出るかというと、ServiceWorker
はセキュリティ上の理由から次の条件が必須になるからです。
- HTTPSで接続している
- しかも証明書は自分で作った証明書じゃダメ!!😫
私の環境でも自己証明書(いわゆるオレオレ証明)でHTTPS
接続をしていたのですが、ブラウザ側に「これじゃ危険だからダメよ!」とエラー表示されることになりました。
では、どうすればいいかというと、例えばGoogle Chrome
の場合は起動の仕方でこの証明書のチェックを無視するようにすることができます。(もちろん普通に起動した場合は、ばっちりセキュリティ完備です)
やりかたは、Google Chrome
が起動するパスにオプション値をつけてコマンドから起動するだけです。
google-chrome --ignore-certificate-errors --unsafely-treat-insecure-origin-as-secure=https://(ローカルのドメイン) --allow-insecure-localhost --user-data-dir=/tmp/foo
※ ちなみに--user-data-dir
はきちんと存在する一時ファイルのフォルダを指定してください。そうでないと、エラーで起動できませんでした。
間違ってプッシュ通知を拒否してしまったら
プッシュ通知の許可を求められたときに間違って拒否したり、ダイアログを消してしまうと、それ以降ブロックされてしまい、以下のようなエラーが表示されることになります。
Notifications permission has been blocked as the user has dismissed the permission prompt several times. This can be reset in Page Info which can be accessed by clicking the lock icon next to the URL.
そんな場合は、(Google Chrome
の場合だと)ブラウザのファビコンが表示される部分をクリックし、通知の部分を「確認(デフォルト)」へ変更して、ページをリロードしてください
開発時の便利機能を使う
キャッシュを無視して最新コードが反映されるようにする
Service Worker
は、ブラウザに登録されることになるのですが、開発中に問題になってくることのひとつで「最新のコードが反映されない」というものがあります。
これを解決するためには、Google Chrome
のDevTools
を開いて(右クリック > 検証)、Applicationタブ > Service Workers をクリックすると以下のようにリロードする度に最新のものにする設定「Update on reload」がありますので、ここにチェックをいれておくといいでしょう。
これで、ブラウザがリロードされたらService Worker
も自動的に最新のものに更新されることになります。
テストのプッシュ通知をする
これもChrome
のDevTools
の機能ですが、擬似的にプッシュ通知されたようにテストすることができます。
キャッシュを無視して最新コードが反映されるようにする で表示したDevToolsのすぐ下に以下のような部分があるので、該当するService Worker
を探してPush
と書かれた項目を好きなテキストに変更し「Push」ボタンをクリックするだけでOKです。
ダウンロードする
今回実際に開発したソースコード一式を以下からダウンロードすることができます。
※ ただし、パッケージのインストールやVAPID
の作成などはご自身で行っていただく必要があります。
おわりに
ということで、今回はLaravel
を使ってプッシュ通知を送る方法をお届けしました。
プッシュ通知の構造上、いくつも開発しなければいけない部分がありますが、Laravel
のパッケージを使うことで、少しは楽に実装することができると思います。
そして、プッシュ通知自体についての私の雑感ですが、スマホアプリのように通知を送ることができるのはちょっとした革命的なイメージがある反面、プッシュ通知を一度でも拒否してしまうと、それを解除するのにちょっとした手間があったりするので、その辺が少し「うーん😅」なところだったりします。
また、個人的にプッシュ通知は「いや、そんなことまで通知しなくていいよ😫」という内容まで送信してくることも多いため、発信者側のモラルが低いと、この機能自体の印象が悪くなってしまい、ユーザーから敬遠されてしまうこともあるんじゃないかとも感じました。
とはいえ、うまく使えば効果的な集客にもつなげることができると思いますので、ぜひ皆さんのサイトでも活用してみてくださいね。
ではでは〜!