FCM + ServiceWorkerでバックグラウンドのプッシュ通知を実装する方法

## はじめに
本記事はできるだけお手軽にwebアプリバックグラウンド通知を実装してみたという内容となっています。
そのため細かい動作はさておき、プッシュ通知がちゃんと表示されこんな感じで使えるんだということをお見せできればと思っています！

## 前提
Googleのアカウントを持っていてFirebaseを使用できること
ブラウザはchromeを使用する

## FCMとServiceWorkerとは
ウェブプッシュ通知の実現には、もちろんプッシュ通知を配信するものと受け取るものが必要です。
今回プッシュ通知を配信する役割を担ってもらうのは、[FCM](https://firebase.google.com/docs/cloud-messaging?hl=ja)（Firebase Cloud Messaging）というGoogleのサービスです。詳しい内容は公式のドキュメント等を確認していただきたいですが、重要な点として配信される[メッセージの形式](https://firebase.google.com/docs/cloud-messaging/concept-options?hl=ja)が３種類あり、どれを使うかによって、実装方法が変わってきますので紹介します。

* 通知メッセージ
    * 送信内容に`notification`フィールドが含まれるメッセージ
    * 決まった項目を入れる必要がある
        * title
            * プッシュ通知のタイトル
        * body
            * プッシュ通知の本文
    * SDKを使うと勝手に処理して上記の内容で表示してくれる
* データメッセージ
    * 送信内容に`data`フィールドが含まれるメッセージ
    * ユーザーが任意の項目名とデータ内容を入れることができる
    * SDKは処理してくれない内容なので、自前で処理を書く必要がある
* 通知メッセージ＋データメッセージ
    * 送信内容に`notification`フィールドと`data`フィールドが含まれるメッセージ
    * 上記二つの特性を併せ持つ通知形式
        * SDKを使うと`notification`フィールド内の項目を勝手に処理して内容でプッシュ通知を表示してくれる
        * `data`フィールド内にユーザーが任意の項目名とデータ内容を入れることができる

今回はできるだけお手軽に通知を表示することが目的なので、通知メッセージとSDKを使用して、プッシュ通知を表示させたいと思います！

プッシュ通知を受け取る役割を担ってもらうのは、[ServiceWorker](https://developer.mozilla.org/ja/docs/Web/API/Service_Worker_API)です。
ウェブアプリケーション、ブラウザ、そしてネットワークの間に介在するプロキシーサーバーのように振る舞うJavaScript環境です。
ServiceWorkerができることは、オフラインキャッシュやバックグラウンドの動機などもありますが、今回はプッシュ通知を受け取る役割として使用します。
このServiceWorker内でFirebaseのSDKを使って通知を受け取り、表示までさせます。


## 構成
今回プッシュ通知を行うための構成図は以下の通りです。

![プッシュ通知＿複数プロファイル.drawio (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524923/e0e943d7-ea7a-45e5-9713-9cdac3f8f2ae.png)

① webアプリからFCMサーバーに対してデバイスを識別するためのトークン取得を行う
② 取得したトークンをstorageに保存する。今回は簡略化のためブラウザのstorageに保存
③ 通知メッセージの内容をFCMサーバーに送る
④ FCMサーバーから①で取得したトークンに対して通知を送り、ServiceWorkerで受信する
⑤ SDKが処理した内容がプッシュ通知として表示される

## 実装内容
必要な実装は以下の通りです。
webアプリ　トークン取得・保存処理とServiceWorkerの登録処理
ServiceWorker SDKのインポート

```js
// main.js (ブラウザで実行されるスクリプト)

import { initializeApp } from "https://www.gstatic.com/firebasejs/11.2.0/firebase-app.js";
import { getMessaging, getToken } from "https://www.gstatic.com/firebasejs/11.2.0/firebase-messaging.js";

// Firebaseプロジェクトの設定
const firebaseConfig = {
    apiKey: "Your_API_Key",
    authDomain: "Your_Auth_Domain",
    projectId: "Your_Project_ID",
    storageBucket: "Your_Storage_Bucket",
    messagingSenderId: "Your_Messaging_Sender_ID",
    appId: "Your_App_ID"
};

// ウェブプッシュ証明書 (VAPID) キー
// Firebaseプロジェクト設定の「クラウドメッセージング」タブで生成したキーです
const vapidKey = "Your_VAPID_Key";

// Firebaseを初期化
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);

// Service Workerファイルのパス
const serviceWorkerUrl = './firebase-messaging-sw.js';

/**
 * Service Workerを登録し、FCMトークンを取得する
 */
async function registerServiceWorkerAndGetToken() {
    // Service Workerが利用可能かチェック
    if (!('serviceWorker' in navigator)) {
        console.warn('Service Workerはブラウザでサポートされていません。');
        return;
    }

    try {
        // Service Workerを登録
        const registration = await navigator.serviceWorker.register(serviceWorkerUrl, { scope: './' });
        console.log('Service Workerが正常に登録されました:', registration);

        // FCMトークンを取得
        const currentToken = await getToken(messaging, {
            serviceWorkerRegistration: registration,
            vapidKey: vapidKey
        });

        if (currentToken) {
            console.log("FCM登録トークン:", currentToken);
            // 取得したトークンをサーバーに送信して保存する処理を実装します
            // 例: localStorage.setItem("fcmToken", currentToken);
        } else {
            console.warn("FCMトークンが取得できませんでした。通知の許可が拒否された可能性があります。");
        }
    } catch (error) {
        console.error('プッシュ通知の初期化中にエラーが発生しました:', error);
    }
}

```

```js
// firebase-messaging-sw.js (Service Workerファイル)
// Firebase SDK のスクリプトをロード
importScripts("https://www.gstatic.com/firebasejs/11.2.0/firebase-app-compat.js");
importScripts("https://www.gstatic.com/firebasejs/11.2.0/firebase-messaging-compat.js");

// Firebaseプロジェクトの設定値を記述
const firebaseConfig = {
    apiKey: "Your_API_Key",
    authDomain: "Your_Auth_Domain",
    projectId: "Your_Project_ID",
    storageBucket: "Your_Storage_Bucket",
    messagingSenderId: "Your_Messaging_Sender_ID",
    appId: "Your_App_ID"
};

// Firebase 初期化 & Messagingインスタンス作成
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
```

### 留意点
* main.jsはアプリの他の処理などを書くと思いますので、適宜修正してください
* 今回は通知機能のみを使う想定なので、スコープは特に意識しない
* main.jsとfirebase-messaging-sw.jsでインポートしているSDKが違っているが、これは以前使われていたAPI形式の互換性のあるSDKか最近推奨されているモジュール形式のSDKかの違いであり、どちらを使ってもプッシュ通知は表示できる
* OSの通知許可（chrome）とブラウザの通知許可をしていないとプッシュ通知が表示されない
* その他細かい挙動などはSDKの仕様に準ずる

## プッシュ通知確認手順

1. ServiceWorkerがちゃんと作成できているかの確認
① 登録処理を書いたアプリのページを開く
② アプリの開発者ページを開く
③ Application > ServiceWorkerを開く
④ 開いたページにServiceWorkerが存在していてactivateな状態になっている
⑤ アプリ内で取得したトークンが保存されていることを確認する

2. 実際に通知を送ってみて表示されるかを確認
今回は、curlコマンドを使って通知を表示させてみます。
3. Google Cloud CLIをインストールする。これはFCMに通知内容を送るときに必要なトークンの取得に使用します
4. FirebaseコンソールからFirebase Admin SDKの秘密鍵を取得する
5. Google Cloud のサービスアカウントを使って gcloud CLI 認証を行う
`gcloud auth activate-service-account --key-file="2-2で取得した秘密鍵のダウンロード先のパス"`
6. gcloud CLI以下のコマンドをたたき、FCMに通知内容を送る際に必要なトークンを取得する。
`gcloud auth print-access-token`
7. 以下のcurlコマンドの組み立てして実行する
```
curl -X POST \
  -H "Authorization: Bearer <6で取得したトークン>" \
  -H "Content-Type: application/json" \
  -d '{
    "message": {
      "token": "1-⑤で確認したトークン",
      "notification": {
      "title": "プッシュ通知テスト",
      "body": "プッシュ通知テスト"
      },
      "webpush": {
        "fcm_options": {
          "link": "任意の遷移先"
        }
      }
    }
  }' \
  https://fcm.googleapis.com/v1/projects/<認証情報を取得したFirebaseのプロジェクト名>/messages:send
```

↓ プッシュ通知表示に成功したときのイメージ画像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3524923/1751e8ba-a894-480c-9589-3a376c051f0e.png)

※ ServiceWorker内で使用しているSDKの仕様でバックグラウンド通知のみ表示する、つまりServiceWorkerを登録したアプリがフォアグラウンドで表示されているときは、ログ上は成功していても表示されません。



### プッシュ通知表示に失敗したときの確認内容例

* 開発者画面のServiceWorkerが、エラーが出ていない、activeな状態になっている
* curlコマンドに入れているトークンが逆になっていないか
* 手順７の実行時にエラーが出ている（トークンの有効期限切れなど）
* 通知設定が拒否になっている

## 終わりに

自分で実際にプッシュ通知を実装する際は、様々な記事を参考にしていましたが、自前でServiceWorker内の処理を書いたりしている記事が多くかなり時間がかかりました。
なので、できるだけ簡単に自前で実装するところが少なくなるような方法で今回は書いてみました。
環境によってはServiceWorkerの登録方法（パスやスコープ）などが異なり上記の方法ではうまくいかないかもしれません。その時はすみません。。。
詰まった場合は、[こちら](https://firebase.google.com/docs/cloud-messaging/js/client?hl=ja)の公式の手順を見ながら適宜修正していただければ幸いです。
ただここでお伝えしたい内容は、意外と簡単にプッシュ通知の仕組みができるんだということです。
このことが伝わりプッシュ通知で悩んでいる人や実装のハードルが高く感じている人の参考になれば幸いです。