Help us understand the problem. What is going on with this article?

Firebase を利用したプッシュ通知の実装

はじめに

仕事で解決したいことがあり、Web Push と Service Worker(以下 SW)の利用を検討しようかなと思っています
調べると Firebase Cloud Messaging(以下 FCM)を使うと簡単に実装できそうだったので、サンプルアプリを作成してみようと思います

今回実装したコードは以下です
https://github.com/kurosame/glossary

FCM の設定(管理画面)

以下から初期設定を行う

glossary_Firebase_console.png

管理画面上での設定は以上で、その後実装する上で必要な情報は「プロジェクトの設定」で確認できます

glossary_Firebase_setting.png

Firebase の初期設定

以下の情報は「プロジェクトの設定」で見れます
隠してますけど、これらの情報は公開してもセキュアです
ただし、Firestore などのリソースにアクセスできてしまうと思うので、使っている場合は適切なルールを設定しておく必要はあります

@/firebase/config.ts
const config = {
  apiKey: '...',
  authDomain: '...',
  databaseURL: '...',
  projectId: '...',
  storageBucket: '...',
  messagingSenderId: '...',
  appId: '...'
}

export default config

初期化後、messaging を export しておきます

@/firebase/index.ts
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 のみで行っています

プッシュ通知許可のダイアログ表示

以下の関数を実行するとブラウザ上にダイアログが出ます

@/utils/messaging.ts
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
webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin')

module.exports = () => ({
  plugins: [new WorkboxPlugin.GenerateSW({ swDest: 'messaging-sw.js' })]
})
@/utils/messaging.ts
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 の状態でトークンを取得するとエラーをスローします

@/utils/messaging.ts
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

サーバーキーは管理画面の「プロジェクトの設定」で見れます

また、管理画面上からでも通知を作成して送れます

glossary_–_Cloud_Messaging_–_Firebase_console.png

もし通知が来なかった場合に確認すること

  • アプリがフォアグラウンドになっている
  • OS のシステム環境設定で Chrome の通知を ON にしているか

もっと使えるプッシュ通知にする

現状だとあまり使い物にならないので、最低限以下は実装してみようと思います

  • アプリがフォアグラウンド時でもプッシュ通知を送る
  • プッシュ通知にクリックイベントを追加する
  • 複数のデバイスにプッシュ通知を送る

アプリがフォアグラウンド時でもプッシュ通知を送る

以下のような自前の SW を使う必要があるので、用意します

@/firebase/messaging-sw.js
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 も修正します

webpack.config.js
module.exports = () => ({
  plugins: [
    new WorkboxPlugin.InjectManifest({
      swSrc: path.join(__dirname, 'src', 'firebase', 'messaging-sw.js'),
      swDest: 'messaging-sw.js'
    })
  ]
})

アプリがフォアグラウンド時にプッシュ通知を受信すると onMessage コールバック関数が呼ばれるので、その中で通知を表示する処理を行います

@/utils/messaging.ts
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 イベントハンドラーを実装して対応します

@/utils/messaging.ts
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))
  )
}
@/firebase/messaging-sw.js
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)

プッシュ通知の本番導入にあたり検討したいこと

自分は PC でウェブサイトを見ようとして、いきなり以下のダイアログを出されると反射的にブロックしてしまいます

 2020-01-16 18.18.30.png

ブロックする理由は以下です

  • 通知の目的が不明
  • ダイアログの位置が邪魔
  • (そもそも通知されるのが嫌な気持ちも少しある)

通知が嫌な場合は無理ですが、通知の目的と位置の問題は工夫できると思います

たとえば、Snackbar を活用して

  1. ウェブサイトを閲覧する上で邪魔でない位置に Snackbar を出す
  2. Snackbar に通知の目的の記載と同意ボタン的なのを設置
  3. 同意ボタンを押したユーザーのみ通知許可ダイアログを出す

↑ 位置はサイトを閲覧する上で邪魔にならず、でも許可までの導線をアピールできる場所が良いです

そもそもユーザーすべてが通知を許可するのは不可能なので、プッシュ通知を許可しないとアプリのメインの機能が使えないという状態は避けるべきですね
あくまでプッシュ通知はプラス α で利用できる便利機能という位置付けだと思います

kurosame
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした