はじめに
仕事で解決したいことがあり、Web Push と Service Worker(以下 SW)の利用を検討しようかなと思っています
調べると Firebase Cloud Messaging(以下 FCM)を使うと簡単に実装できそうだったので、サンプルアプリを作成してみようと思います
今回実装したコードは以下です
https://github.com/kurosame/glossary
FCM の設定(管理画面)
以下から初期設定を行う
管理画面上での設定は以上で、その後実装する上で必要な情報は「プロジェクトの設定」で確認できます
Firebase の初期設定
以下の情報は「プロジェクトの設定」で見れます
隠してますけど、これらの情報は公開してもセキュアです
ただし、Firestore などのリソースにアクセスできてしまうと思うので、使っている場合は適切なルールを設定しておく必要はあります
const config = {
apiKey: '...',
authDomain: '...',
databaseURL: '...',
projectId: '...',
storageBucket: '...',
messagingSenderId: '...',
appId: '...'
}
export default config
初期化後、messaging を export しておきます
import firebase from 'firebase/app'
import 'firebase/messaging'
import config from '@/firebase/config'
const firebaseApp = firebase.initializeApp(config)
export const messaging = firebaseApp.messaging()
FCM の設定(TS)
ここからは、以下の公式ドキュメントに書かれている内容を見ながら実装します
https://firebase.google.com/docs/cloud-messaging/js/client?authuser=0
今回はトークンを管理しないので、onTokenRefresh コールバック関数の実装は省略してます
また、VAPID も使いません
cURL で FCM のサーバーキーを Authorization リクエストヘッダーに含めて FCM サーバーに POST して動作確認します
通知のタイプに「通知メッセージ」と「データメッセージ」の 2 種類がありますが、今回は「通知メッセージ」を使います
-
通知メッセージ
- アプリがフォアグラウンドでの通知は自前で表示させる実装が必要
- アプリがバックグラウンドでの通知は SDK を使用して自動表示される
-
データメッセージ
- フォアグラウンド、バックグラウンドに限らず通知は自前で表示させる実装が必要
動作確認は Chrome のみで行っています
プッシュ通知許可のダイアログ表示
以下の関数を実行するとブラウザ上にダイアログが出ます
export default function initialize(): void {
Notification.requestPermission().then(p => {
console.info(p)
})
}
パラメーターのp(permission)
はダイアログに対して行ったアクションのコールバックです
型定義を見るとtype NotificationPermission = "default" | "denied" | "granted";
となっており、この 3 つのいずれかの文字列が入っています
-
granted
許可した場合 -
denied
ブロックした場合 -
default
ダイアログの選択を無視した場合(ブラウザをリロードなど)
default の場合のみダイアログが表示される仕様となっています
SW のデプロイ
通知の許可を得られたら、次はトークンの取得を行います
しかし、その前に SW を設定する必要があります
FCM の getToken 関数内で以下の SW を登録している処理があるからです
navigator.serviceWorker.registe(DEFAULT_SW_PATH, { scope: ...})
DEFAULT_SW_PATH の値は/firebase-messaging-sw.js
です
よって、ドキュメントルートにfirebase-messaging-sw.js
ファイルを置く必要があります
ローカルで動作確認中であれば、dist 等のパブリックディレクトリに置けばオッケーです
ただ実用性を考えると、アプリは webpack を使ってバンドルしているので、webpack で SW をいい感じに処理してデプロイしてくれるやつがほしいです
SW の後々の拡張性を考えて、Workbox を使うことにしました
npm i -D workbox-webpack-plugin
const WorkboxPlugin = require('workbox-webpack-plugin')
module.exports = () => ({
plugins: [new WorkboxPlugin.GenerateSW({ swDest: 'messaging-sw.js' })]
})
import { messaging } from '@/firebase/index'
export default async function initialize(): Promise<void> {
// ここから
if ('serviceWorker' in navigator) {
await navigator.serviceWorker
.register('/messaging-sw.js')
.then(reg => messaging.useServiceWorker(reg))
.catch(err => console.error(err))
}
// ここまで追加
Notification.requestPermission().then(p => {
console.info(p)
})
}
トークンの取得
通知を許可すると currentPermission が granted となり、トークンが取得できるようになります
currentPermission が denied の状態でトークンを取得するとエラーをスローします
import { messaging } from '@/firebase/index'
export default async function initialize(): Promise<void> {
...
Notification.requestPermission().then(p => {
// ここから
if (p === 'granted') {
messaging
.getToken()
.then(t => console.info(t))
.catch(err => console.error(err))
}
// ここまで追加
})
}
実行するとコンソールにトークンが出力されます
生成されたトークンはブラウザの IndexedDB に保存され、再利用されます
IndexedDB からトークンオブジェクトを削除すると getToken 時に再度新しいトークンを生成します
動作確認
プッシュ通知が来るか確認
アプリがバックグラウンド時のみ動作します
フォアグラウンド時に通知を機能させるには onMessage イベントハンドラーの実装が必要になります(後述)
curl -X POST \
-H "Authorization: key=${サーバーキー}" \
-H Content-Type:"application/json" \
-d '{
"notification": {
"title":"test",
"body":"testtest"
},
"to": "${上記で取得したトークン}"
}' \
https://fcm.googleapis.com/fcm/send
サーバーキーは管理画面の「プロジェクトの設定」で見れます
また、管理画面上からでも通知を作成して送れます
もし通知が来なかった場合に確認すること
- アプリがフォアグラウンドになっている
- OS のシステム環境設定で Chrome の通知を ON にしているか
もっと使えるプッシュ通知にする
現状だとあまり使い物にならないので、最低限以下は実装してみようと思います
- アプリがフォアグラウンド時でもプッシュ通知を送る
- プッシュ通知にクリックイベントを追加する
- 複数のデバイスにプッシュ通知を送る
アプリがフォアグラウンド時でもプッシュ通知を送る
以下のような自前の SW を使う必要があるので、用意します
importScripts('https://www.gstatic.com/firebasejs/6.2.4/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/6.2.4/firebase-messaging.js')
firebase.initializeApp({ messagingSenderId: '...' })
const messaging = firebase.messaging()
messagingSenderId は記事の上の方で@/firebase/config.ts
に書いたやつと同じです
webpack の WorkboxPlugin も修正します
module.exports = () => ({
plugins: [
new WorkboxPlugin.InjectManifest({
swSrc: path.join(__dirname, 'src', 'firebase', 'messaging-sw.js'),
swDest: 'messaging-sw.js'
})
]
})
アプリがフォアグラウンド時にプッシュ通知を受信すると onMessage コールバック関数が呼ばれるので、その中で通知を表示する処理を行います
import { messaging } from '@/firebase/index'
export default async function initialize(): Promise<void> {
...
// ここから
messaging.onMessage(
(payload: { notification: { title: string; body: string } }) =>
navigator.serviceWorker.ready
.then(reg =>
reg.showNotification(`${payload.notification.title}(foreground)`, {
body: payload.notification.body
})
)
.catch(err => console.error(err))
)
// ここまで追加
}
serviceWorker.ready
を使うことで SW の activate を待って処理できます
cURL で通知を POST するとアプリがフォアグラウンド時でもプッシュ通知が表示されると思います
アプリがバックグラウンド時に POST した場合は、変わらず SDK によって自動生成された通知を表示します(onMessage は呼ばれません)
通知にクリックイベントを追加する
リクエストに click_action を追加します
curl -X POST \
-H "Authorization: key=${サーバーキー}" \
-H Content-Type:"application/json" \
-d '{
"notification": {
"title":"test",
"body":"testtest",
"click_action":"${HTTPSのURL}"
},
"to": "${上記で取得したトークン}"
}' \
https://fcm.googleapis.com/fcm/send
アプリがバックグラウンド時の場合は、SDK を使用して、これだけでクリックイベントが発火します
アプリがフォアグラウンド時の場合は、SW に notificationclick イベントハンドラーを実装して対応します
import { messaging } from '@/firebase/index'
export default async function initialize(): Promise<void> {
...
messaging.onMessage(
(payload: {
notification: {
title: string
body: string
click_action: string // これを追加
}
}) =>
navigator.serviceWorker.ready
.then(reg =>
reg.showNotification(`${payload.notification.title}(foreground)`, {
body: payload.notification.body,
data: payload.notification.click_action // これを追加
})
)
.catch(err => console.error(err))
)
}
importScripts('https://www.gstatic.com/firebasejs/6.2.4/firebase-app.js')
importScripts('https://www.gstatic.com/firebasejs/6.2.4/firebase-messaging.js')
firebase.initializeApp({ messagingSenderId: '...' })
const messaging = firebase.messaging()
// ここから
self.addEventListener('notificationclick', e => {
e.notification.close()
e.waitUntil(clients.openWindow(e.notification.data))
})
// ここまで追加
複数のデバイスにプッシュ通知を送る
トークンをトピックに紐付けることで実現可能です
トピックには複数のトークンを紐付けることができます
そして、トピックを指定してプッシュ通知を送り、複数デバイスへの通知を実現できます
トピックへのトークン追加はリクエストヘッダーにサーバーキーを含むため、サーバー側で処理します
今回はサーバーを用意してないので、cURL を使って追加します
curl -X POST \
-H "Authorization: key=${サーバーキー}" \
-H Content-Type:"application/json" \
https://iid.googleapis.com/iid/v1/${トークン}/rel/topics/${トピック名}
Firebase プロジェクトに存在しないトピックを指定した場合は、そのトピックを新規作成します
トークンが subscribe しているトピックは以下で確認できます
curl -X POST \
-H "Authorization: key=${サーバーキー}" \
https://iid.googleapis.com/iid/info/${トークン}?details=true
後は、to
にトークンではなく、トピックを指定して POST すれば、通知が届くはずです
curl -X POST \
-H "Authorization: key=${サーバーキー}" \
-H Content-Type:"application/json" \
-d '{
"notification": {
"title":"test",
"body":"testtest",
"click_action":"${HTTPSのURL}"
},
"to": "/topics/${トピック名}"
}' \
https://fcm.googleapis.com/fcm/send
サーバーでトークンをトピックに追加する場合
実際にサーバーで処理する場合は、Firebase Admin SDK が使えます
今回はサーバーを用意してないので試してませんが、たとえば Node.js であれば以下のような実装になります
import admin from 'firebase-admin'
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
})
admin.messaging().subscribeToTopic(registrationTokens, topic)
- 参考
https://firebase.google.com/docs/admin/setup?hl=ja#initialize_the_sdk
https://firebase.google.com/docs/cloud-messaging/js/send-multiple#subscribe_the_client_app_to_a_topic
プッシュ通知の本番導入にあたり検討したいこと
自分は PC でウェブサイトを見ようとして、いきなり以下のダイアログを出されると反射的にブロックしてしまいます
ブロックする理由は以下です
- 通知の目的が不明
- ダイアログの位置が邪魔
- (そもそも通知されるのが嫌な気持ちも少しある)
通知が嫌な場合は無理ですが、通知の目的と位置の問題は工夫できると思います
たとえば、Snackbar を活用して
- ウェブサイトを閲覧する上で邪魔でない位置に Snackbar を出す
- Snackbar に通知の目的の記載と同意ボタン的なのを設置
- 同意ボタンを押したユーザーのみ通知許可ダイアログを出す
↑ 位置はサイトを閲覧する上で邪魔にならず、でも許可までの導線をアピールできる場所が良いです
そもそもユーザーすべてが通知を許可するのは不可能なので、プッシュ通知を許可しないとアプリのメインの機能が使えないという状態は避けるべきですね
あくまでプッシュ通知はプラス α で利用できる便利機能という位置付けだと思います