Vue + Laravel + Firebaseで始めるWeb Push通知

## はじめに
業務で **FCM（Firebase Cloud Messaging）**を使ったWeb Push通知をSPAで実装する機会がありました。
この記事では、いくつかポイントを設けながら段階的に実装できるように書きたいと思います。
またLaravelと記載していますが、HTTPクライアントライブラリのGuzzleを使ってリクエストを投げるだけです。

## この記事で説明する範囲
今回VueやLaravel、その他のライブラリなどの詳細な説明は省きます。あくまでもFCMを使ったフォアグラウンド・バックグラウンドへのPush通知がメインです。

## FCM（Firebase Cloud Messaging）とは
公式ページには
> Firebase Cloud Messaging（FCM）は、メッセージを無料で確実に送信するためのクロスプラットフォーム メッセージング ソリューションです。

と記載されています。
今回はWebを対象としますが仕組みは共通で、通知を受信する端末が一意なトークンをFCM側に発行してもらい、送信時に対象のトークンを用いてFCMを経由し、Push通知を送るという仕組みで、Firebaseが提供しているサービスの一つです。

## 手順
1. Firebaseでのプロジェクト作成・アプリ登録
2. Voluntary Application Server Identification鍵（VAPID鍵）の取得
3. 通知に対するPermissionの実装
4. フォアグラウンド・バックグラウンドでの通知の受信
5. Guzzleでリクエストを投げる


[Firebase を JavaScript プロジェクトに追加する](https://firebase.google.com/docs/web/setup?hl=ja)
上記のリンクを確認しながら進めていけば問題ないかなと思います。
Laravelであれば [Firebase SDK を追加して Firebase を初期化する](https://firebase.google.com/docs/web/setup?hl=ja#add-sdks-initialize)という部分に関しては、Vueインスタンスを表示するbladeでFirebase SDKを追加しましょう。


<head>
    <script src="https://www.gstatic.com/firebasejs/6.5.0/firebase-app.js"></script>
    <script src="https://www.gstatic.com/firebasejs/6.5.0/firebase-messaging.js"></script>
```
また初期化に関しては、私の場合`resource/js/plugins/firebase.js`などのようにディレクトリを構成し、

```firebase.js
if (!firebase.apps.length) {
    firebase.initializeApp({
        apiKey: process.env.MIX_FIREBASE_API_KEY,
        authDomain: process.env.MIX_FIREBASE_AUTH_DOMAIN,
        databaseURL: process.env.MIX_FIREBASE_DATABASE_URL,
        projectId: process.env.MIX_FIREBASE_PROJECT_ID,
        storageBucket: process.env.MIX_FIREBASE_STORAGE_BUCKET,
        messagingSenderId: process.env.MIX_FIREBASE_MESSAGING_SENDER_ID,
        appId: process.env.MIX_FIREBASE_APP_ID,
    })
}

export default firebase
```
exportしてオブジェクトを各ファイルで参照できるようにしていますが、オブジェクトを使用したいファイルで直接初期化を行っても問題ありません。


## 2. Voluntary Application Server Identification鍵（VAPID鍵）の取得・設定
Web Push通知をWebアプリケーションに飛ばす際FirebaseプロジェクトとWebアプリケーションを紐づける必要があります。Firebaseコンソールから新しく生成する方法、既存のリソースをインポートする方法の両方用意されています。
<img width="1299" alt="鍵生成.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656421/da88300d-5272-4411-0d70-0004beddf84b.png">

VAPID鍵を生成・インポートしたら`resource/js/App.vue`に早速コードを書いていきます。
最初に私の場合は`resource/js/plugins/firebase.js`でFirebaseの初期化を行ったので、そちらのファイルをimportし、ライフサイクルの中で`firebase.messaging`を呼び出します。
これでメッセージングサービスを使用する事ができます。

```App.vue
import Firebase from './plugins/firebase'

export default {
    created() {
        const messaging = Firebase.messaging()
```

上記のようにメッセージオブジェクトを呼び出した後に、**Voluntary Application Server Identification鍵（VAPID鍵）の取得**を行った際に生成・インポートしたVAPID鍵をコードに追加します。

```App.vue
export default {
    created() {
        const messaging = Firebase.messaging()
        messaging.usePublicVapidKey(process.env.MIX_FIREBASE_APP_VAPPI_KEY)
```

こちらの`usePublicVapidKey`メソッドを使用するとFCMで各種プッシュサービスにメッセージリクエストを送信するときにVAPID鍵の認証情報を使用できます。


## 3. 通知に対するPermissionの実装
実際にPush通知を送信する為に、ユーザーに許可を要請する必要があります。
そのためには`requestPermission`メソッドを使用してユーザーに対する要請の結果をPromissで取得します。

```App.vue
        const messaging = Firebase.messaging()
        messaging.usePublicVapidKey(process.env.MIX_FIREBASE_APP_VAPPI_KEY)

        Notification.requestPermission()
            .then(permission => {
                console.log('Have permission')
            })
            .catch(err => {
                console.log('Error Occured')
            })
```

この段階でローカル環境の画面にアクセスすると
<img width="427" alt="PermissionのRequest.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656421/20e1ed05-6d2a-c420-e89c-c2ac0684ee07.png">
このような形で**通知を表示**のダイアログが確認できると思います。
またこの段階で許可をタップすると、ブラウザのコンソールに`Have permission`と表示されることも確認できるでしょう。

次のステップに移る前にサービスワーカーファイルを作成しておきましょう。
ここでは`firebase-messaging-sw.js`というファイル名で作成することが決まっているので、そのファイル名でファイルを作成したらドメインのルートに置きます。Laravelであれば、`public/`配下にファイルを設置します。今は作ったファイルは空でも構いません。

## 4. フォアグラウンド・バックグラウンドでの通知の受信
まず始めにFCMから発行されるトークンを取得します。先ほど書いたユーザーへの許可を要請した処理で、成功した時にトークンを取得するメソッドを呼び出すだけです。

```App.vue
        Notification.requestPermission()
            .then(permission => {
                console.log('Have permission')
                return messaging.getToken()
            })
            .then(token => {
                console.log(`firebase cloud messaging token: ${token}`)
            })
            .catch(err => {
                console.log('Error Occured')
            })
```
ここまで書いて画面にアクセスするとトークンの取得が出来ていれば
<img width="798" alt="getToken.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656421/6288b69f-8b08-d8cd-9707-bb454b3d3436.png">
このように表示されていると思います。
前述しましたが、こちらのFCMから発行されたトークンを使用して、対象の端末に通知を表示させます。

ここでPush通知が表示される際に2つのパターンがある事を認識しておく必要があります。

* 1つ目はWebブラウザが閉じられていたり、ブラウザが閉じられていたりした場合は、3の**通知に対するPermissionの実装**の最後で作成したサービスワーカーファイル内でメッセージが受け取られます。
* 2つ目はユーザーがWebページを表示している時です。この場合は実際のWebページ内でメッセージを受け取ります。なのでメッセージを受け取った際に何かのイベント（ラベルやマーカーの表示）を考えている場合は、メッセージを受け取るイベントで処理を書く必要があります。

それではメッセージを受け取るために、空で作成したサービスワーカーファイルにFirebaseメッセージングService Worker定義しましょう。
messagingSenderIdには上記のFirebaseを初期化した時に利用した値を指定してあげる形で大丈夫です。

```firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/6.3.4/firebase-messaging.js')

firebase.initializeApp({
    messagingSenderId: 'hogehogehoge'
})

const messaging = firebase.messaging()
```
ここでも特に変わった事はなく、Firebaseの初期化の後に`messaging`を呼び出すだけです。


### フォアグラウンドでの受信
次にこの章の目的でもあるフォアグランドでの通知の取得を`onMessage`イベントを呼び出して確認してみましょう。

```App.vue
        Notification.requestPermission()
            .then(permission => {
                console.log('Have permission')
                return messaging.getToken()
            })
            .then(token => {
                console.log(`firebase cloud messaging token: ${token}`)
            })
            .catch(err => {
                console.log('Error Occured')
            })

        messaging.onMessage(function(payload) {
            console.log('onMessage: ', payload)
        })
```

ここまで出来たらFCMのAPIにcurlでリクエストを投げて挙動を確認してみましょう。

```
curl -X POST \
-H "Authorization: key={MIX_FIREBASE_SERVER_KEY}" \
-H "Content-Type: application/json" \
-d '{
    "notification": {
        "title": "Hello",
        "body": "World",
        "icon": ""
    },
    "to": "{firebase return token}"
}'  \
"https://fcm.googleapis.com/fcm/send"
```
上記を実行すると
<img width="961" alt="Foregrandメッセージ表示.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656421/68053f68-d028-daed-c367-2ad2c671555e.png">
こちらのように表示されていて、通知をWebページ内で受信している事が確認できると思います。


### バックグラウンドでの受信
バックグラウンドで受信する際には、通知を受信するWebページにフォーカスがない状態をブラウザで作る（別タブで他のページを閲覧している）か、他のアプリにフォーカスが当たっている状態で先ほどのリクエストを投げると挙動が確認できると思います。

<img width="1277" alt="Backgroundでのメッセージ.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/656421/33e6e800-f484-e7d5-6add-667dc22bc20c.png">

確認できましたね。

今回は対象としませんが、通知オプションを設定したい場合（データペイロードを使用する場合など）は[こちらを参照してください](https://firebase.google.com/docs/cloud-messaging/js/receive?hl=ja#setting_notification_options_in_the_service_worker)
基本的には、サービスワーカーファイルにメソッドを一つ追加して、データオプションに設定するだけです。


## Guzzleでリクエストを投げる
ほぼおまけの内容ですが、先ほどのcurlで投げたリクエストを、Guzzleで投げる際のサンプルコードぐらいにみていただけると嬉しいです。

```php
public static function handle($token, $content, $link)
    {
        $client = new Client();

        $server_key = env('MIX_FIREBASE_SERVER_KEY');

        $url = https://hogehogehogehoge;

        $fcm_endpoint = 'https://fcm.googleapis.com/fcm/send';

        $headers = [
            'Authorization' => 'key=' . $server_key,
            'Content-Type' => 'application/json'
        ];

        $fields = [
            'notification' => [
                'title' => 'Sample-Notification',
                'body' => $content,
                'icon' => asset('img/icon/logo-main.png'),
                'click_action' => $url . $link
            ],
            'to' => $token
        ];

        $client->post($fcm_endpoint, [
            'headers' => $headers,
            'json' => $fields
        ]);
    }
```

## 最後に
今回FCMを触るきっかけとなった実装では、フォアグラウンドでの処理に[vue-notification](https://github.com/se-panfilov/vue-notifications)を使用して、ユーザーになんらかのアクションを行う導線を実装したりしました。
無料で始められ手軽に実装できるので使う際の利点は多そうです。