Progressive Web Apps (PWA) 学習者のメモ その2  (プッシュ通知とFCM)

# この文章について


PWAを構成する主要技術のひとつ、プッシュ通知(Push API)について調べたメモです。
合わせて、Firebase を利用したプッシュ通知の実装を行ってみました。

誤記などありましたらご指摘ください。

(2019年2月時点のメモとなります)

PWAの Service Worker については[こちらの記事](https://qiita.com/TakeshiNickOsanai/items/8d012a128827c9db980d)をご覧ください。



# プッシュ通知について

Web技術を利用した通知用APIは

- [Notifications API](https://developer.mozilla.org/ja/docs/Web/API/Notifications_API)
- [Push API](https://developer.mozilla.org/ja/docs/Web/API/Push_API)

の2つがある。PWAの世界で「プッシュ通知」について説明する場合、後者の「Push API」を利用したプッシュ通知を指す場合が多い

# Push APIを利用したプッシュ通知の仕組み

[Google のドキュメント](https://developers.google.com/web/fundamentals/push-notifications/how-push-works?hl=ja)を元にまとめると

<b>初期設定：</b>

1. サーバー側でプッシュ通知を行うためのアプリケーション・サーバーキー(VAPID)を準備する


<b>プッシュ通知のサブスクリプション：</b>

1. エンドユーザー(Web訪問者、PWAユーザー)が通知の受信許可を行う
2.  [PushSubscription](https://w3c.github.io/push-api/#push-subscription)　がエンドユーザーから送信
    -  PushSubscription には、ユーザーを個別に認識する情報が含まれる
3. PushSubscriptionをサーバー側で保存する
4. プッシュ通知を受け取るためのスクリプトをService Worker に登録し、通知の受信体制を作る


<b>プッシュ通知の送信</b>

1. サーバーからプッシュ通知を送信
2. PushSubscription に基づき、通知登録をしたユーザーへ個別に通知が送られる
3. ユーザーがオンラインになったとき、Service Worker を通じてデリバーされた通知をデバイス上に表示する



## Firebase Cloud Messaging (FCM)

[Firebase](https://firebase.google.com/?hl=ja) には [Firebase Cloud Messaging (FCM)](https://firebase.google.com/docs/cloud-messaging/?hl=ja)というプッシュ通知機能がある。FCMを利用することで、PWAに対してプッシュ通知を送ることができる。

プッシュ通知を送信するサーバーと、プッシュ通知を届けるアプリケーションは別個となる。

([Firebase](https://firebase.google.com/?hl=ja) のプッシュ通知サービス、[Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/?hl=ja) は両者が一体となっているため把握しづらいが、プッシュ通知のデリバー用サーバーとアプリは別物となる)


ネット上には、PWAのプッシュ通知に [Firebase](https://firebase.google.com) を利用しているケースが多いが、Firebase を使わなくともプッシュ通知の実装は行える。

Googleのコードラボでは、[プッシュ通知用のテストサーバーを利用したチュートリアル](https://developers.google.com/web/fundamentals/codelabs/push-notifications/?hl=ja)が公開されている。

# Firebase を利用したプッシュ通知の実装

FCMと [Firebase JavaScript SDK](https://github.com/firebase/firebase-js-sdk) を利用したプッシュ通知を実装してみる。


## 大まかな仕様

- プッシュ通知を受け取るだけのアプリ
- ユーザーが「購読」ボタンを押すことで、プッシュ通知のサブスクリプションを行う。
- エンドポイントへPOSTを行うアプリを別途phpで開発、通知を行いときに実行を行う


## Firebase の設定

Firebase で新しいプロジェクトを作成し、「プロジェクトの設定」をクリック
![fb_0.png](https://qiita-image-store.s3.amazonaws.com/0/83767/e37c7463-ad55-e9e2-b426-f15041dded69.png)

Firebase の「クラウドメッセージング」タブから、サーバーキーと ウェブプッシュ証明書の鍵ペア(VAPIDKey) を取得

<a href="https://qiita-image-store.s3.amazonaws.com/0/83767/8e04fb65-a41f-3607-4456-227daee475f5.png">
<img src="https://qiita-image-store.s3.amazonaws.com/0/83767/8e04fb65-a41f-3607-4456-227daee475f5.png" width="480">
</a>


<a href="https://qiita-image-store.s3.amazonaws.com/0/83767/24e9e06b-3d91-07a7-8148-b363099764c4.png">
<img src="https://qiita-image-store.s3.amazonaws.com/0/83767/24e9e06b-3d91-07a7-8148-b363099764c4.png" width="480">
</a>

## コード

index.html

```html
<!DOCTYPE HTML>
<html lang="ja">

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>プッシュ通知の購読</title>
  <link rel="manifest" href="./manifest.json">
  <link rel="stylesheet" type="text/css" href="./bootstrap.min.css">
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-app.js"></script>
  <script src="https://www.gstatic.com/firebasejs/5.5.2/firebase-messaging.js"></script>
</head>

<body>

  <div class="col-sm-3"></div>

  <div class="col-sm-6">
    <h1 class="text-center">プッシュ通知の購読</h1>
    <h2 id="newitem" class="text-center"></h2>
    <button id="button" onclick="getSubscription()" class="btn center-block"></button>
  </div>

  <div class="col-sm-3"></div>

  <script>

    // Firebase のSDKを利用し、SenderIDを設定して初期化

    const config = {
      messagingSenderId: "SenderIDを記述"
    };
    firebase.initializeApp(config);
    const messaging = firebase.messaging();
    messaging.usePublicVapidKey("ウェブプッシュ証明書キーを記述");

// Service Worker ファイルを登録し、ボタン表示を行う
    registSW();
    initialButton();

    function initialButton() {
      messaging.getToken().then(token => {
        if (token) {
          document.getElementById("button").innerText = "プッシュ通知を購読中";
        } else {
          document.getElementById("button").innerText = "プッシュ通知を受け取る";
        }
      }).catch(function (err) {
        console.log('An error occurred while retrieving token. ', err);
      });
    }

// トークンが未取得の場合 = プッシュ通知を未購読の場合、プッシュ通知の登録許可を行う
// すでに購読済みの場合、取得済みのFirebase用トークンを表示

    function getSubscription() {
      messaging.getToken().then(token => {
        if (!token) {
          getNotification();
        } else {
          displayToken();
        }
      }).catch(function (err) {
        console.log('An error occurred while retrieving token. ', err);
      });
    }

//  Firebase のSDKを使い、プッシュ通知の購読処理を行う

    function getNotification() {
      messaging.requestPermission().then(function () {
        console.log('Notification permission granted.');
        displayToken();
        initialButton();
      }).catch(function (err) {
        console.log('Unable to get permission to notify.', err);
      });
    }

//　トークン表示

    function displayToken() {
      messaging.getToken().then(token => {
        if (token) {
          console.log(token);
        } else {
          console.log('No Instance ID token available. Request permission to generate one.');
        }
      }).catch(function (err) {
        console.log('An error occurred while retrieving token. ', err);
      });
    }

//　Service Worker ファイルを登録

    function registSW() {

      // Service Worker ファイル「firebaes-messaging-sw.js」を登録する

      if ('serviceWorker' in navigator) {
        window.addEventListener('load', function () {
          navigator.serviceWorker.register('/firebase-messaging-sw.js')
            .then(function (registration) {
              console.log('firebase-messaging-sw.js registration successful with scope: ', registration.scope);
            }, function (err) {
              console.log('firebase-messaging-sw.js registration failed: ', err);
            });
        });
      }
    }
  </script>
</body>

</html>

```

manifest.json

Firebase のFCMを利用する場合、gcm_sender_idを記述する必要がある。
JSON内の「gcm_sender_id」は、103953800507で固定で、変更できない。

```json
{
  "short_name": "Firebase のプッシュ通知購読アプリ",
  "name": "Firebaseのプッシュ通知購読アプリ",
  "start_url": "index.html",
  "gcm_sender_id": "103953800507"
}

```

FCMを利用して Service Worker を登録する場合、プッシュ通知を受信するためのサービスワーカーは、ファイル名を必ず「firebase-messaging-sw.js」としなければいけない。また、必ずドメイン直下のルートディレクトリに配置しなければいけない。

firebase-messaging-sw.js内でSDKを利用するため、SDKをインポートし、初期化処理を行う必要がある。


firebase-messaging-sw.js

```javascript
importScripts('https://www.gstatic.com/firebasejs/5.5.2/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.5.2/firebase-messaging.js');

firebase.initializeApp({
  'messagingSenderId': 'SenderIDを記述'
});

const messaging = firebase.messaging();

messaging.setBackgroundMessageHandler(function (payload) {
  console.log('[firebase-messaging-sw.js] Received background message ', payload);
  // Customize notification here
  let notificationTitle = 'Background Message Title';
  let notificationOptions = {
    body: 'Background Message body.',
  };

  return self.registration.showNotification(notificationTitle,
    notificationOptions);
});
```


## 通知用のアプリ

プッシュ通知を送るための、phpコード例。


send.php

```php
<?php

$json = '{
"notification":
{
    "title": "テスト送信",
    "body": "This Is 送信テストです"
},
"to": "index.htmlで取得したトークンを記述"}';

$ch = curl_init();

$headers = array(
    'Content-Type: application/json',
    'Authorization: key=Firebaseの秘密キーを記述'
);

curl_setopt_array($ch, array(
    CURLOPT_URL => 'https://fcm.googleapis.com/fcm/send',
    CURLOPT_HTTPHEADER => $headers,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => $json
));

$response = curl_exec($ch);

curl_close($ch);

?>

```

phpを実行すると、Firebase の Service Worker がメッセージを取得し、通知表示のを行う

![fb_3.png](https://qiita-image-store.s3.amazonaws.com/0/83767/a6cf3d46-bce9-ff7e-450d-1ca17ce68739.png)


# 参考情報

## MDN

MDNのプッシュ通知に関するリファレンス
https://developer.mozilla.org/ja/docs/Web/API/Push_API

## Firebase Cloud messaging のリファレンス

https://firebase.google.com/docs/reference/js/firebase.messaging?hl=ja
https://firebase.google.com/docs/reference/js/firebase.messaging.Messaging?hl=ja

## Qiitaの記事

https://qiita.com/rubytomato@github/items/1c07a109f580c138bce7
https://qiita.com/ryo_hisano/items/1171beca22d5a04ed802
https://qiita.com/tomoyukilabs/items/2ae4a0f708a1af75f13e


## 購読解除はまだ手探り状態？

購読したプッシュ通知を解除する方法はないかと調べたところ、「unsubscribe」というメソッドが見つかった。
https://developer.mozilla.org/ja/docs/Web/API/PushSubscription/unsubscribe

しかし、対応ブラウザがまだ少なく、Chromeでも挙動が安定しなかった。
まだ使える環境が整っていない感じ。
数年後には標準的なメソッドになっているかもしれない。



## 実装あれこれ

今回の実装では、トークンを取得後、一回ずつphpを実行しているが、実際には

・プッシュ通知を購読したユーザーのトークンをサーバー側へ送信
・サーバー側のDBにトークンを登録
・DBの登録に対して、プッシュ通知を行うループ処理を実行

という流れになりそうです。

FCM用の Service Worker ファイルと、アプリ側のService Worker ファイルを分けて、2つの Service Worker を別個に登録することもできる。

以下のような感じ

```javascript
function registSW() {

  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
      navigator.serviceWorker.register('./sw.js', { scope: './' })
        .then(function (registration) {
          console.log('SW.js registration successful with scope: ', registration.scope);
        }, function (err) {
          console.log('Sw.js registration failed: ', err);
        });
    });
  }


  if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
      navigator.serviceWorker.register('/firebase-messaging-sw.js')
        .then(function (registration) {
          console.log('firebase-messaging-sw.js registration successful with scope: ', registration.scope);
        }, function (err) {
          console.log('firebase-messaging-sw.js registration failed: ', err);
        });
    });
  }
}
```

必要以上に Service Worker ファイルを分割すると、管理が大変そうなので、気をつける必要がありそう。