React(Vite)+FCMでプッシュ通知を実装した際の注意点

今回、自身の開発しているサービスをPWA化してプッシュ通知まで実装しようと頑張ったのですが、Vite環境で実装している記事が見つからなかったり、FCM(Firebase Cloud Messaging)の仕様が変更されていて記事が当てにならなかったりしたのでメモ程度に共有します。
誰かの助けになれば...(未来の自分の助けになっているかも)

前提

前提として、React+Viteのプロジェクトが一旦作成されていることを前提としています。
そこからわからなければ公式のドキュメント形を見て作成してください。
本記事における環境は

Vite: "^5.1.4"
React: "^18.2.0"
TypeScript: "^5.2.2"
Firebase: "^10.8.1"

です。 また、同様にFirebaseのプロジェクトも作成されていること前提です。

実際の実装

通知を受け取るところの実装

/*
   JavaScript クライアントでメッセージを受信する  |  Firebase
   https://firebase.google.com/docs/cloud-messaging/js/receive?hl=ja
 */

// Give the service worker access to Firebase Messaging.
// Note that you can only use Firebase Messaging here, other Firebase libraries
// are not available in the service worker.
importScripts('https://www.gstatic.com/firebasejs/8.2.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/8.2.1/firebase-messaging.js');

// APIキーはコードに含めても問題ないみたい
// https://firebase.google.com/docs/projects/api-keys?hl=ja
firebase.initializeApp({
  apiKey: "************************",
  authDomain: "******.firebaseapp.com",
  projectId: "******",
  storageBucket: "******.appspot.com",
  messagingSenderId: "************",
  appId: "****************************",
  measurementId: "**************"
});

// バックグラウンドメッセージを処理できるようにFirebase Messagingのインスタンスを取得
const messaging = firebase.messaging();

messaging.onBackgroundMessage((payload) => {
  console.log('[firebase-messaging-sw.js] Received background message ', payload);
  // notificationがある場合は、自動的に表示されるので、ここで表示する必要はない
  // 2重で表示されるので注意
  
  // const notificationTitle = payload.notification.title;
  // const notificationOptions = {
  //   body: payload.notification.body,
  //   icon: "/icon-512-any.png"
  // };

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

// Workbox を使用してサービスワーカーにキャッシュリストを注入するためのプレースホルダーとして機能します。
// injectManifest 戦略を使う場合、Workbox はビルド時にこのプレースホルダーを見つけ、
// その位置に実際のキャッシュリスト(プレキャッシュマニフェスト)を注入 By ChatGPT
console.log(self.__WB_MANIFEST);
import { getToken, onMessage, getMessaging } from "firebase/messaging"
import { useEffect } from "react"
import { messaging, useAuth } from "../contexts/AuthContext"
import { userService } from "../services"

const app = initializeApp(firebaseConfig)
const messaging = getMessaging(app)

export const Notification = () => {

  useEffect(() => {
    if (!user) return
    updateMessagingToken()
    onMessage(messaging, (payload) => {
      console.log(payload)
    })
  }, [user])

  // トークンをサーバー側へ通達する処理
  const updateMessagingToken = async () => {
    try {
      const token = await getToken(messaging, { vapidKey: import.meta.env.VITE_FIREBASE_VAPID_KEY })
      if (!token) return
      await userService.updateNotificationToken(token)
    } catch (error) {
      console.error(error)
    }
  }

  return (
    <></>
  )
}

export default Notification

通知を送信する実装

import firebase_admin.messaging as messaging

def send_notification_to_user(user_token: str, title: str, body: str):
    if token is None:
        return
    message = messaging.Message(
        webpush=messaging.WebpushConfig(
            notification=messaging.WebpushNotification(
                title=title,
                body=body,
                icon="https://storage.googleapis.com/[projectId].appspot.com/icon.png",
            ),
        ),
        token=user_token,
    )
    messaging.send(message)

詰まったところ

Service Workerの登録

Viteで構築していたので、VitePWAというプラグインでservice workerを自動注入していたが、firebaseのservice workerの挿入方法がわからなかった。
いろいろ調べた結果、プラグインの設定を重ね掛けすることで2つ挿入することに成功した。

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      strategies: 'injectManifest',
      filename: 'firebase-messaging-sw.js',
      injectManifest: {
        swSrc: 'public/firebase-messaging-sw.js',
        swDest: 'dist/firebase-messaging-sw.js',
      },
      workbox: {
        globPatterns: [],
        globIgnores: ['*'],
      },
    }),
    VitePWA({ 
      registerType: 'autoUpdate',
      injectRegister: 'auto',
      manifest: {
        ...
      }
    })
  ],
})

Https化

公式やいろんなサイトではhttps化、すなわちSSL通信でないと通知が送信されないと書いてあることが多いが、僕の環境だと普通にhttp://localhostで立ち上げた開発環境のアプリに通知は飛んできた。
参考に楽なhttps化

FCMの仕様変更

いろいろな方が書いてくれてる記事のときからFCM(Firebase Cloud Messaging)の仕様が変更されていた。
https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ja
僕が調べながらぶち当たった壁をいくつか

  • POSTMANで試しにAPI送信してみたとき
    • エンドポイントの変更
    • Authorizationの方法が違う(key==>Authorization Bearer)
    • ペイロードの構造の変更(notificationがmessageに入ったり)
  • firebase.initializeAppのときにサーバーキー?だけでは認証情報不足に
  • notificationはフォアグラウンドのみ、PWAでバックグラウンド通知したい場合はwebpush煮含める

Firebaseのバージョン整合

ここが一番詰まったところ!
アプリを起動しているときのフォアグラウンドの通知には成功するのに、chrome://inspect/#service-workersから確認できるバックグラウンドの通知が全然つかめなかった。
これで数日悩んだ結果、Reactからインポートしているfirebaseのバージョンとservice workerでインポートしているfirebaseのバージョンの互換性がないことが原因だった。
今回はこのバージョンの組み合わせで行けたので放置しているが、メジャーバージョンがどこまで違いが許容されるのかはあまり理解していない。(service workerの方をバージョン10にしてみたがないよ!って言われた)