FCM + ServiceWorker でPUSH通知のメモ

# 背景
__PUSH通知__という言葉に惹かれ、独自に勉強を始めた。しかし、これがとても苦労したのでメモを残す。

# 前提
* 正しい組み込み方ではな無いかもしれない。
* とりあえず動いた！という喜びの勢いでメモ残している。
* ServiceWorkerを利用するにはSSLでページにアクセスする必要があるので、その環境があること。

## ディレクトリ構造

```:DIRECTORY構成
/(ROOT)
 ├ index.html
 ├ firebase-messaging-sw.js
 ├ app.js
 ├ manifest.json
 ├ script/
 │   └ firebase-init.js
 ├ images/
     ├ 144.png
     └ 192.png

```
# 手順
## index.htmlを作成する

index.htmlはページを表示するとともに、__必要なJavascriptを読み込む__ことが目的となる。今回の場合はfirebaseを利用する為、__firebaseを利用する為に必要なJavascript__と__serviceworkerを利用する為の処理が書かれたJavascript__を読み込むことになる。

```html:index.html
<!DOCTYPE html>
<html>

  <head>
    <meta charset="UTF-8">

    <!-- manifest.json を指定 -->
    <link rel="manifest" href="/manifest.json">

    <!-- firebaseを利用する為のJavascript -->
    <script defer src="https://www.gstatic.com/firebasejs/7.3.0/firebase-app.js"></script>
    <script defer src="https://www.gstatic.com/firebasejs/7.3.0/firebase-messaging.js"></script>
    <script defer src="/script/firebase-init.js"></script>

    <!-- ServiceWorkerを利用する為のJavascript -->
    <script defer src="app.js"></script>
  </head>

  <body>
    <h1>SAMPLE ServiceWorker - 00</h1>
  </body>

</html>
```

## manifest.json

manifest.jsonは、本アプリの名称などを定義する役割がある。ファイル名自体に意味は無いようだが、json形式で定義する必要がある。定義する項目については割愛するが、FCMを利用したPUSH通知に必要な項目としては__gcm_sender_id__がある。この値は__103953800507__でとりあえず固定らしい。何故かは知らないが、そのうち調べる。

```json:manifest.json
{
    "name": "Service Worker Sample",
    "short_name": "sw sample",
    "icons": [
        {
            "src": "/images/144.png",
            "sizes": "144x144",
            "type": "image/png"
        },
        {
            "src": "/images/192.png",
            "sizes": "192x192",
            "type": "image/png"
        }
    ],
    "start_url": "/",
    "display": "standalone",
    "theme_color": "#e91e63",
    "gcm_sender_id": "103953800507",
}
```
今回は通知しか試さないため、iconsの項目は不要かも。

## firebase-init.js
firebase-init.jsの名称に指定は無い。とりあえずFirebaseの初期化の意味を込めている。

```javascript:script/firebase-init.js
  // firebaseConfigの内容は、firebaseのコンソールから取得出来る。
  var firebaseConfig = {
    apiKey: "[APIKEYAPIKEYAPIKEYAPIKEYAPIKEY]",
    authDomain: "[AUTHDOMAINAUTHDOMAINAUTHDOMAIN]",
    databaseURL: "[DATABASEURLDATABASEURLDATABASEURL]",
    projectId: "[PROJECTIDPROJECTIDPROJECTID]",
    storageBucket: "[STORAGEBUCKETSTORAGEBUCKETSTORAGEBUCKET]",
    messagingSenderId: "[MESSAGINGSENDERID]",
    appId: "[APPIDAPPIDAPPID]"
  };

  // Initialize Firebase
  firebase.initializeApp(firebaseConfig);

  const messaging = firebase.messaging();
  messaging.usePublicVapidKey('[PUBLICVAPIDKEYPUBLICVAPIDKEYPUBLICVAPIDKEYPUBLICVAPIDKEY]');


  messaging.onMessage( payload => {
    // WEBアプリがフォアグラウンドの状態で通知を受信するとonMessageが呼び出される。
    console.log("onMessage")


  })

messaging.onTokenRefresh(() => {
    //トークンが更新されるとonTokenRefreshが呼び出される。
    messaging.getToken().then((refreshedToken) => {
	console.log(refreshedToken)
    }).catch((err) => {
	console.log('Unable to retrieve refreshed token ', err);
	showToken('Unable to retrieve refreshed token ', err);
    });
});

```

理解出来ていない部分も多いがとりあえずこんな感じで動いた。firebasConfigの内容はfirebaseのコンソールから取得出来るのでコピペでよい。
ただし、コピペで得られる情報には__messagingSenderId__が含まれていないので追加する必要がある。firebaseのコンソールより、クラウドメッセージングへ進むと__送信者ID__という項目があるので、その値を用いる。
更に```messaging.usePublicVapidKey```に与えるキーもfirebaseのコンソールから__ウェブプッシュ証明書__を生成して取得する。

## app.js
ServiceWorkerを登録する処理を行う。具体的には以下の点をおさえておけばよい。

* ブラウザのServiceWorker対応状況
* 通知許可をユーザに仰ぐ
* 許可されたら、通知の購読(サブスクリプション)を登録する

```javascript:app.js
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function () {

    navigator.serviceWorker.register('/firebase-messaging-sw.js')
      .then(registration => {

        //通知の許可をユーザに確認
        Notification.requestPermission()
          .then(permission => {

            messaging.getToken().then(
              token => {
                console.log(token)
              })


            if (permission === 'granted') {
              //通知が許可されている場合
              console.log('granted!!!!!')
              navigator.serviceWorker.ready.then(p => {

                p.pushManager.getSubscription().then(subscription => {

                  if (subscription === null) {

                    //通知の購読が存在しない場合は登録する。
                    let re = p.pushManager.subscribe({
                      userVisibleOnly: true
                    })

                  }
                })

              })

            } else {
              //通知が許可されなかった場合  
              console.log(permission)
            }
          })
      })
  })
}

```

## firebase-messaging-sw.js
firebase-messaging-sw.jsはバックグラウンドで動作するスクリプト。ブラウザに登録されてバックグラウンドで独立して動作するので、firebaseの利用に必要なJavascriptの読込処理などは全て記載する必要がある（という事なのかな）。

大切なのは以下の点。

* PUSH通知を受信した際のイベントを登録する
* PUSH通知をバックグラウンドで受信した際のイベントを登録する

```javascript:firebase-messaging-sw.js

importScripts("https://www.gstatic.com/firebasejs/7.3.0/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.3.0/firebase-messaging.js");

// firebaseConfigの内容は、firebaseのコンソールから取得出来る。
var firebaseConfig = {
  apiKey: "[APIKEYAPIKEYAPIKEYAPIKEYAPIKEY]",
  authDomain: "[AUTHDOMAINAUTHDOMAINAUTHDOMAIN]",
  databaseURL: "[DATABASEURLDATABASEURLDATABASEURL]",
  projectId: "[PROJECTIDPROJECTIDPROJECTID]",
  storageBucket: "[STORAGEBUCKETSTORAGEBUCKETSTORAGEBUCKET]",
  messagingSenderId: "[MESSAGINGSENDERID]",
  appId: "[APPIDAPPIDAPPID]"
};

// Initialize Firebase
firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

// 通知を受けとると push イベントが呼び出される。
self.addEventListener('push', function (event) {

  console.log("event:push")
  let messageTitle = "MESSAGETITLE"
  let messageBody = "MESSAGEBODY"
  let messageTag = "MESSAGETAG"

  const notificationPromise = self.registration.showNotification(
    messageTitle,
    {
      body: messageBody,
      tag: messageTag
    });

  event.waitUntil(notificationPromise);

}, false)

// WEBアプリがバックグラウンドの場合にはsetBackGroundMessageHandlerが呼び出される。
messaging.setBackgroundMessageHandler(function (payload) {

  console.log("backgroundMessage")

  let messageTitle = "MESSAGETITLE"
  let messageBody = "MESSAGEBODY"

  return self.registration.showNotification(
    messageTitle,
    {
      body: messageBody,
      tag: messageTag
    });
});
```


``self.registration.showNotification``を呼び出すことによって__通知が作成される__。引数にはタイトルとオプション(本文など)を渡す。

``messageTitle``や``messageBody``は固定値にしているが、本来は``event``や``payload``から取得し、適切な情報にする。後述する「Firebaseのコンソールから通知の送信」「curlコマンドによる通知の送信」を試してみるとわかるが、タイトルや本文を入力するフィールドがある。

## 検証

### Firebaseのコンソールから通知の送信

Firebaseのコンソールより、__Cloud Messaging__を開くと通知を作成することが出来るので、タイトルと本文を入力し、ターゲットのアプリを設定すると送信が実行できる。

### curlコマンドによる通知の送信

```bash:curlを利用してFirebase経由で通知を送信する
curl -X POST \
--header "Authorization: key=[SERVERKEY] \
--header Content-Type:"application/json" \
https://fcm.googleapis.com/fcm/send \
-d @- << EOF
{
  "notification": {
    "title":"test",
    "body":"テストメッセージ"
    },
  "to": "[DEVICETOKEN]"
}
EOF
```
* SERVERKEYはFirebaseのコンソールから取得する。
* DEVICETOKENはapp.jsかfirebase-init.jsでトークンを取得した際にブラウザのコンソールに出力されるので、コピペする。


### ブラウザ（Chrome）
デベロッパーツールを開き、ApplicationタブでServiceWorkerの状態を確認することが出来る。上手く動作しないときは__Unregister__リンクを押し再度ページを読み込む。通知を受信するとConsoleタブに"event:push","onMessage","backgroundMessage"のいずれかが出力される。

正しく動作していれば、ブラウザを最小化したり、ページタブを閉じていても通知ダイアログが表示される。

# その他
正しく理解していないがとりあえず通知を表示することが出来たのでメモとして残した。その他、気になったことや苦戦した点をココに残しておく。

## ``messaging.setBackgroundMessageHandler``は、ほとんど呼び出されることがなかった。

これは[Firebaseのドキュメント](https://firebase.google.com/docs/cloud-messaging/js/topic-messaging?hl=ja)にも記載がある。送信されてきた情報に通知フィールド(notification)が存在する場合はsetBackgroundMessageHandlerが呼び出されないらしい。前述した「curlコマンドによる通知の送信」に手を加え、notificationをdataに書き換えてみると、setBackgroundMessageHandlerが呼び出された。

```bash:curlを利用してFirebase経由で通知を送信する(notification項目無し)
curl -X POST \
--header "Authorization: key=[SERVERKEY] \
--header Content-Type:"application/json" \
https://fcm.googleapis.com/fcm/send \
-d @- << EOF
{
  "data": {
    "title":"test",
    "body":"テストメッセージ"
    },
  "to": "[DEVICETOKEN]"
}
EOF
```

## 通知ダイアログが表示されるのはWEBアプリがフォアグラウンド状態ではない時？

動作確認には苦労した。そもそも、何が正しい動作なのかを理解していなかった。通知ダイアログが表示されるのはWEBアプリがフォアグラウンド状態ではない時だけのようだった（…もしかしたら、この理解も間違っているかもしれない）。そして、どの状態がフォアグラウンドでどの状態がそうでないのかよくわからなかったので、「ウィンドウの最小化」や「タブを閉じる」とか、明らかにフォアグラウンドでない状態にするようにした。