前回Vite PWAでPush通知を試すのだでVitePWAつかって簡単にPWAアプリをつくれて、サービスワーカーも簡単に実装できることがわかった。もちろん、Push通知もできる。
実際のPush通知にあたりセグメンテーションしたりサブスク情報を管理するのが大変なので、Firebase Cloud Messaging(FCM)を使うことになりそう、どういうことでFCMを試してみた🐦
先に成果物
こんなのつくった。
https://github.com/11bluetree/vite-pwa-fcm-app
やってみる
前回https://github.com/11bluetree/vite-pwa-push-notice-appで作ったアプリをベースにFCMに切り替えた。
また開発環境も前回同様Windows VS Code Dev Container(Node.js)を使用した。
Firebaseのインストール
firebaseのライブラリをインストールする。
npm i firebase
Firebaseプロジェクトの作成
ライブラリ入れても通知できないまずはFirebaseのプロジェクトを作成する。
「Firebaseプロジェクトを設定して開始」をクリック。
testプロジェクトを作成した。Googleアナリティクスは今のところ不要なのでoffにした。
FCMの設定
続いてMessagingサービスを選択して設定する。
今回はWebアプリなので「ウェブ」を選択する。
アプリ名は「test-app」とした。
作成したら、初期化コードが表示されるのでそれを利用する。
Firebaseの初期化
環境変数から読み込むようにした。値については先ほどの初期化コードで確認できる。
import { initializeApp } from "firebase/app";
import { getMessaging } from "firebase/messaging";
const firebaseConfig = {
apiKey: import.meta.env.VITE_API_KEY,
authDomain: import.meta.env.VITE_AUTH_DOMAIN,
projectId: import.meta.env.VITE_PROJECT_ID,
storageBucket: import.meta.env.VITE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_APP_ID
};
const app = initializeApp(firebaseConfig);
const messaging = getMessaging(app);
FCM Service Workerの作成
前回はVAPIDキーを使ったPush通知だったが、今回はFCMを使う。
FCMではFirebaseが提供する仕組みを使うので、Service WorkerでもFirebase SDKを使う必要がある。
src/sw-fcm-push.ts
をFCM対応で作成する。
import { precacheAndRoute } from 'workbox-precaching'
interface ExtendedServiceWorkerGlobalScope extends ServiceWorkerGlobalScope {
addEventListener: (type: string, listener: (event: Event) => void) => void
registration: ServiceWorkerRegistration
clients: {
openWindow: (url: string) => Promise<unknown>
}
}
declare let self: ExtendedServiceWorkerGlobalScope
// Workbox のプリキャッシュを設定
precacheAndRoute(self.__WB_MANIFEST)
// Firebase App と Messaging をインポート
import { initializeApp } from 'firebase/app'
import { getMessaging, onBackgroundMessage } from 'firebase/messaging/sw'
import getFirebaseConfig from './firebase-config-shared'
// Firebase 設定(環境変数から取得)
const firebaseConfig = getFirebaseConfig()
// Firebase を初期化
const app = initializeApp(firebaseConfig)
const messaging = getMessaging(app)
// FCM バックグラウンド メッセージの処理
onBackgroundMessage(messaging, (payload) => {
console.log('[sw-fcm-push.ts] Received background message ', payload)
// 通知の内容をカスタマイズ
const notificationTitle = payload.notification?.title || 'FCM Background Message'
const notificationOptions = {
body: payload.notification?.body || 'FCM Background Message body.',
icon: payload.notification?.icon || '/favicon.svg',
badge: '/favicon.svg',
data: {
...payload.data,
fcm_message_id: payload.messageId,
dateOfArrival: Date.now(),
primaryKey: 1
},
actions: [
{
action: 'open',
title: '開く',
icon: '/favicon.svg'
},
{
action: 'close',
title: '閉じる',
icon: '/favicon.svg'
}
],
vibrate: [100, 50, 100]
}
self.registration.showNotification(notificationTitle, notificationOptions)
})
// 通知クリックイベントのリスナー
self.addEventListener('notificationclick', (event: Event) => {
const notificationEvent = event as unknown as {
notification: {
close: () => void
data?: Record<string, unknown>
}
action?: string
waitUntil: (promise: Promise<void>) => void
}
console.log('[sw-fcm-push.ts] Notification click received.', notificationEvent)
notificationEvent.notification.close()
if (notificationEvent.action === 'open') {
// アプリを開く
notificationEvent.waitUntil(
self.clients.openWindow('/').then(() => undefined)
)
} else if (notificationEvent.action === 'close') {
// 通知を閉じるだけ
console.log('Notification closed by user action.')
} else {
// デフォルトのクリック動作 - アプリを開く
notificationEvent.waitUntil(
self.clients.openWindow('/').then(() => undefined)
)
}
})
ポイントは以下:
-
Firebase SDK for Service Workers:
firebase/messaging/sw
からonBackgroundMessage
をインポート - バックグラウンド通知: アプリがバックグラウンドにあるときのメッセージ処理
- 通知のカスタマイズ: タイトル、本文、アイコン、アクションボタンを設定
-
Workbox統合:
precacheAndRoute
でキャッシュ管理も継続
FCMとVAPIDの違いとして、FCMはFirebaseのインフラを使うため、VAPIDキーの管理やメッセージの配信をFirebaseが代行してくれる。
vite.config.tsでService Workerのファイル名を指定することも忘れずに📝
export default defineConfig({
plugins: [react(), VitePWA({
strategies: 'injectManifest',
srcDir: 'src',
filename: 'sw-fcm-push.ts', // FCM対応のSWファイル名
// ... 他の設定
})],
})
通知コンポーネントの作成
FCM用の通知コンポーネントを作成する。VAPIDキーの代わりにFCMトークンを管理する。
まずsrc/services/fcm-service.ts
でFCMサービスを作成。
import { getToken, onMessage, deleteToken, Unsubscribe } from 'firebase/messaging';
import { messaging } from '../firebase-config';
export interface FCMTokenResult {
token: string | null;
error?: string;
}
export interface FCMMessage {
notification?: {
title?: string;
body?: string;
icon?: string;
};
data?: Record<string, string>;
}
/**
* 通知権限を要求する
*/
export const requestNotificationPermission = async (): Promise<NotificationPermission> => {
if (!('Notification' in window)) {
throw new Error('このブラウザは通知をサポートしていません');
}
if (Notification.permission === 'granted') {
return Notification.permission;
}
const permission = await Notification.requestPermission();
return permission;
};
/**
* FCM トークンを取得する
*/
export const getFCMToken = async (): Promise<FCMTokenResult> => {
try {
// 通知権限を確認・要求
const permission = await requestNotificationPermission();
if (permission !== 'granted') {
return {
token: null,
error: '通知の権限が許可されていません'
};
}
// Service Worker の登録を確認
if (!('serviceWorker' in navigator)) {
return {
token: null,
error: 'Service Worker がサポートされていません'
};
}
// Service Worker が ready になるまで待機
const registration = await navigator.serviceWorker.ready;
// FCM トークンを取得(カスタム Service Worker を使用)
const token = await getToken(messaging, {
serviceWorkerRegistration: registration,
});
if (!token) {
return {
token: null,
error: 'FCM トークンの取得に失敗しました'
};
}
console.log('FCM Token:', token);
return { token };
} catch (error) {
console.error('FCM トークン取得エラー:', error);
return {
token: null,
error: error instanceof Error ? error.message : 'FCM トークンの取得に失敗しました'
};
}
};
/**
* フォアグラウンドでのメッセージを監視する
*/
export const onForegroundMessage = (callback: (payload: FCMMessage) => void): Unsubscribe => {
return onMessage(messaging, (payload) => {
console.log('フォアグラウンドメッセージを受信:', payload);
callback(payload);
});
};
/**
* FCM トークンを削除して購読を完全に解除する
*/
export const deleteFCMToken = async (): Promise<{ success: boolean; error?: string }> => {
try {
if (!messaging) {
return {
success: false,
error: 'Firebase Messaging が初期化されていません'
};
}
await deleteToken(messaging);
return {
success: true
};
} catch (error) {
console.error('FCM トークンの削除に失敗:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'トークンの削除に失敗しました'
};
}
};
続いてsrc/components/FCMNotification.tsx
でUIコンポーネントを作成。
import { useState, useEffect, useCallback } from 'react'
import {
getFCMToken,
onForegroundMessage,
showLocalNotification,
copyTokenToClipboard,
deleteFCMToken,
FCMMessage,
FCMTokenResult
} from '../services/fcm-service'
import './FCMNotification.css'
const FCMNotification: React.FC = () => {
const [tokenResult, setTokenResult] = useState<FCMTokenResult>({ token: null })
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [lastMessage, setLastMessage] = useState<FCMMessage | null>(null)
const [copySuccess, setCopySuccess] = useState(false)
// FCM 購読を開始
const startSubscription = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const result = await getFCMToken()
setTokenResult(result)
if (result.error) {
setError(result.error)
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'FCM の購読開始に失敗しました'
setError(errorMessage)
setTokenResult({ token: null, error: errorMessage })
} finally {
setIsLoading(false)
}
}, [])
// フォアグラウンドメッセージの監視
useEffect(() => {
if (!tokenResult.token) return
const unsubscribe = onForegroundMessage((payload: FCMMessage) => {
console.log('フォアグラウンドメッセージ受信:', payload)
setLastMessage(payload)
// フォアグラウンドでもブラウザ通知を表示
if (payload.notification) {
showLocalNotification(
payload.notification.title || 'FCM 通知',
{
body: payload.notification.body,
icon: payload.notification.icon || '/favicon.svg',
data: payload.data
}
)
}
})
return () => {
unsubscribe()
}
}, [tokenResult.token])
return (
<div className="fcm-notification">
<h3>Firebase Cloud Messaging</h3>
<div className="status">
<p>🔔 購読状態: {tokenResult.token ? '✅ 購読中' : '❌ 未購読'}</p>
</div>
<div className="controls">
{!tokenResult.token ? (
<button onClick={startSubscription} disabled={isLoading}>
{isLoading ? '🔄 購読開始中...' : '🤚 プッシュ通知を開始'}
</button>
) : (
<div className="fcm-active">
<button onClick={copyTokenToClipboard}>
📋 トークンをコピー
</button>
</div>
)}
</div>
{/* トークン表示エリア */}
{tokenResult.token && (
<details className="token-details">
<summary>🔑 FCM トークン</summary>
<div className="token-container">
<textarea
readOnly
value={tokenResult.token}
className="token-display"
rows={6}
/>
<p className="token-info">
💡 このトークンを使用して、Firebase Console や サーバーから通知を送信できます
</p>
</div>
</details>
)}
</div>
)
}
export default FCMNotification
主な機能:
- FCMトークン取得: ユーザーの同意を得てFCMトークンを取得
- フォアグラウンド通知: アプリがアクティブな時のメッセージ表示
- トークン管理: トークンのコピー、表示、削除機能
- エラーハンドリング: 権限エラーやネットワークエラーの適切な処理
App.tsxでFCMNotificationコンポーネントを組み込む。
import FCMNotification from './components/FCMNotification'
function App() {
return (
<>
{/* 他のコンポーネント */}
<FCMNotification />
<PWABadge />
</>
)
}
ビルド・プレビュー
Firebase設定を環境変数で管理するので、.env
ファイルを作成する。
VITE_API_KEY=your_api_key_here
VITE_AUTH_DOMAIN=your_project.firebaseapp.com
VITE_PROJECT_ID=your_project_id
VITE_STORAGE_BUCKET=your_project.appspot.com
VITE_MESSAGING_SENDER_ID=your_sender_id
VITE_APP_ID=your_app_id
値はFirebase Consoleの「プロジェクトの設定」→「全般」→「マイアプリ」から確認できる。
Dev環境で開発する場合はdevOptions.enabled: true
にしておく。
// vite.config.ts
export default defineConfig({
plugins: [react(), VitePWA({
// ... 他の設定
devOptions: {
enabled: true, // 開発環境でもSWを有効化
navigateFallback: 'index.html',
suppressWarnings: true,
type: 'module',
},
})],
})
ビルド・プレビューする。
npm run build
npm run preview -- --host
ブラウザでhttp://localhost:4173/を開くとFCM対応のPWAアプリが表示される。
「プッシュ通知を開始」ボタンをクリックすると通知権限を求められ、許可するとFCMトークンが発行される。
このときPush通知が来ているはず。
FCMからの通知テスト
Messagingサービスでキャンペーンを作成してテスト通知を送信できる。
「最初のキャンペーンを作成」をクリック。
「Firebase Notification メッセージ」を選択。
通知内容を設定して次へ
(怖いメッセージだね🥳)
ターゲットはtest-appにする。
オプションはつけずに確認ボタンをクリック。
メッセージの再確認をしたら公開する。
アプリで通知開始していたら通知が届くはず。
まとめ
- FCM統合: Firebase SDKを使ってService WorkerとReactコンポーネントでFCMを実装できた
- トークン管理: FCMトークンの取得、表示、削除機能を実装
-
バックグラウンド通知:
onBackgroundMessage
でアプリがバックグラウンドの時の通知処理 -
フォアグラウンド通知:
onMessage
でアプリがアクティブの時の通知処理 - Firebase Console: GUIで簡単に通知テストができる
- API対応: REST APIでプログラマブルに通知送信も可能
Vite PWA + FCMの組み合わせで、本格的なプッシュ通知対応PWAが作れることが確認できた。
VAPIDキーを自分で管理する必要がなく、Firebaseのインフラを活用できるのが大きなメリット。
次は通知のセグメンテーションやスケジュール配信なども試してみたい。
番外編 Firebase MCPをつかって通知してみる
Firebase MCP サーバーなるものがあった。
これはFirebaseのプロジェクト管理や各種サービスの実行ができるMCPでMessageの送信もできとあった。
なので試してみた。
Firebase CLIのインストール
npm install -g firebase-tools
Firebase CLI を認証
npx -y firebase-tools@latest login --reauth
MCP クライアントを設定する
VS Codeをつかっているのでワークスペースの.vscode/mcp.jsonに記載・実行。
(ユーザー単位で設定するならC:\Users[ユーザー名]\AppData\Roaming\Code\User\mcp.jsonに記載してね)
ついでにブラウザでFCMトークン取得してもらうためにPlaywrightも設定しておく。
{
"servers": {
"firebase": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"firebase-tools@latest",
"experimental:mcp"
]
},
"playwright": {
"command": "npx",
"args": [
"@playwright/mcp@latest"
],
"type": "stdio"
},
}
}
ツールが多すぎるとCopilotだとエラーになるのでmessaging_send_messageだけにする。
メッセージ送るように指示する
あとはCopilot Agentで「#messaging_send_message テスト通知して」と指示するだけ。
以下のことを勝手にやってくれた。ちゃんと通知も届いた。
- サーバー起動
- Playwrightブラウザ起動
- ブラウザ操作でFCMトークン取得
- トークン使ってメッセージ送信
以上!!