効果的!Laravelでブラウザにプッシュ通知する機能

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、この間のコピペでOK!Laravelからwordpressに投稿する機能という記事ではLaravelに独自のNotificationを作ってwordpressに投稿する機能を実装しました。

そして、これに関連してNotificationで他に何かできないかと考えていたところ、ちょっと手間が大きいので「また今度にしよう😉」と考えていた機能を思い出しました。

それが、

プッシュ通知(WebPush)

です。

プッシュ通知とは、ある特定の通知がリアルタイムでユーザーにお知らせされる通知のことで、WebPushはなんとブラウザでサイトを開いていなくても通知させることができるようになります。

そして実は、最近のブラウザにはプッシュ通知機能が実装されているのを知っているでしょうか。(ブラウザのサポート状況はCan I useをご覧ください)

そこで!

今回はブラウザにプッシュ通知を送るWebPushを我らがLaravelで実装してみたいと思います。

ぜひ皆さんのお役に立てると嬉しいです😊✨

開発環境: Laravel 6.x、Google Chrome 79

やりたいこと

今回はプッシュ通知を使って、「ユーザーの皆さん、新しいイベントが登録されましたよ!クリックしてイベントのページを見に来てね😊✨」というお知らせができるようにしてみたいと思います。

では、やっていきましょう!

前提として

今回の記事は、Laravelのログイン機能がすでにインストールされていることを前提としています。

そのため、もしインストールがまだの方は以下を参考にして準備しておいてください。

また、プッシュ通知はローカル環境であっても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.phpHasPushSubscriptionsという名前のトレイトを追加します。

<?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側に 送信することになります。

なお、送信先は、WebPushControllerstore()です。

プッシュ通知を解除する

すでにプッシュ通知が許可されている場合に表示される「イベントのプッシュ通知を解除する」ボタンがクリックされたときに実行されるのが、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());

});

すると、次のような通知が来ました!(OSLinuxなので、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はセキュリティ上の理由から次の条件が必須になるからです。

  1. HTTPSで接続している
  2. しかも証明書は自分で作った証明書じゃダメ!!😫

私の環境でも自己証明書(いわゆるオレオレ証明)で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 ChromeDevToolsを開いて(右クリック > 検証)、Applicationタブ > Service Workers をクリックすると以下のようにリロードする度に最新のものにする設定「Update on reload」がありますので、ここにチェックをいれておくといいでしょう。

これで、ブラウザがリロードされたらService Workerも自動的に最新のものに更新されることになります。

テストのプッシュ通知をする

これもChromeDevToolsの機能ですが、擬似的にプッシュ通知されたようにテストすることができます。

キャッシュを無視して最新コードが反映されるようにする で表示したDevToolsのすぐ下に以下のような部分があるので、該当するService Workerを探してPushと書かれた項目を好きなテキストに変更し「Push」ボタンをクリックするだけでOKです。

ダウンロードする

今回実際に開発したソースコード一式を以下からダウンロードすることができます。

※ ただし、パッケージのインストールやVAPIDの作成などはご自身で行っていただく必要があります。

Laravelでブラウザにプッシュ通知する
開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はLaravelを使ってプッシュ通知を送る方法をお届けしました。

プッシュ通知の構造上、いくつも開発しなければいけない部分がありますが、Laravelのパッケージを使うことで、少しは楽に実装することができると思います。

そして、プッシュ通知自体についての私の雑感ですが、スマホアプリのように通知を送ることができるのはちょっとした革命的なイメージがある反面、プッシュ通知を一度でも拒否してしまうと、それを解除するのにちょっとした手間があったりするので、その辺が少し「うーん😅」なところだったりします。

また、個人的にプッシュ通知は「いや、そんなことまで通知しなくていいよ😫」という内容まで送信してくることも多いため、発信者側のモラルが低いと、この機能自体の印象が悪くなってしまい、ユーザーから敬遠されてしまうこともあるんじゃないかとも感じました。

とはいえ、うまく使えば効果的な集客にもつなげることができると思いますので、ぜひ皆さんのサイトでも活用してみてくださいね。

ではでは〜!

このエントリーをはてなブックマークに追加       follow us in feedly