Nuxt+Firebase Cloud Messaging(FCM)でWebプッシュ通知を送る

WebでもFCMが使えるようになったので、試してみたときの備忘録。
これでSafari以外には、通知が送れるようになる(*´ω｀*)

### 使い方

構成は、@nuxtjs/pwaでPWA化している感じ。

#### コンソール側

Settingsでウェブプッシュ証明書を作成して、鍵ペアを取得する。

<img width="973" alt="スクリーンショット_2020-05-24_17_06_33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/d17f3a0e-e6df-73a7-d57b-a6ff36e7c79f.png">


これを、PUBLIC_VAPID_KEYという環境変数に設定しておく。

#### クライアント側

##### firebaseの初期化

`~/plugins/firebase.ts`というファイルを用意し、firebaseを初期化

```typescript
// ~/plugins/firebase.ts
import * as firebase from "firebase/app";
import "firebase/auth";
import "firebase/firestore";
import "firebase/messaging";


if (!firebase.apps.length) {
  // まずは、firebaseの初期化
  firebase.initializeApp({
    apiKey: process.env.API_KEY,
    authDomain: process.env.AUTH_DOMAIN,
    databaseURL: process.env.DATABASE_URL,
    projectId: process.env.PROJECT_ID,
    storageBucket: process.env.STORAGE_BUCKET,
    messagingSenderId: process.env.MESSAGING_SENDER_ID,
    appId: process.env.APP_ID,
    measurementId: process.env.MEASUREMENT_ID
  });

  // Push通知をサポートしているかをチェック
  // サポートしていないと、firebase.messaging()を呼んだときに例外が発生
  const isSupported = firebase.messaging.isSupported();
  
  // コンソールで発行した、ウェブプッシュ証明書の鍵ペアを取得
  const publicVapidKey = process.env.PUBLIC_VAPID_KEY;
  if (!!publicVapidKey && process.client && !!isSupported) {
    // FCMの初期化。鍵ペアを設定する
    const messaging = firebase.messaging();
    messaging.usePublicVapidKey(publicVapidKey);
    
    // @nuxtjs/pwaが生成するsw.jsと、
    // 後で作成するFCM受信処理用のsw-firebase-messaging.jsを統合するための設定
    navigator.serviceWorker
      .register("/sw.js")
      .then(registration => messaging.useServiceWorker(registration))
      .catch(err => console.error(err));
  }
}

export default firebase;
```

##### トークンの取得

Push通知を送る際の宛先として、トークンを指定しないといけないので取得。
トークンは端末ごとに取得する必要があるので、注意が必要。
※PCとスマホだとそれぞれトークンが違う


```typescript
import firebase from "~/plugins/firebase";

// トークンの取得
public async getToken(user: User) {
  const isSupported = firebase.messaging.isSupported();
  if (!isSupported) return;
  const token = await firebase.messaging().getToken();

  // firestoreにトークンを保存しておく処理(中身は略)
  await saveToken(user, token);
}
```

Firestoreなどへユーザごとにトークンを保存しておく。

`firebase.messaging().getToken();`を呼んだ際に、
通知の設定が「確認」だと、許可を求めるダイアログが表示される。

![スクリーンショット 2020-05-24 16.30.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/e9976414-8418-09c1-8846-25211a949776.png)

これが許可されていないと、トークンも取得できない。

![スクリーンショット 2020-05-24 16.30.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/c21078c8-38d0-04c7-133a-b5402611443d.png)

##### メッセージを受け取ったときの処理

メッセージを受信したときに受け取る関数は2つあり、

- フォアグラウンド(画面を見ている時) ... onMessage
- バックグラウンド(画面を見ていない時) ... setBackgroundMessageHandler

・【参考】[JavaScript クライアントでメッセージを受信する  |  Firebase](https://firebase.google.com/docs/cloud-messaging/js/receive#handle_messages_when_your_web_app_is_in_the_background)

今回はバックグラウンドのときに通知を送りたいので、
`setBackgroundMessageHandler`を設定していく。

こんな感じ。

ドキュメントを見ると、『`firebase-messaging-sw.js`というファイル名で作成』と書かれているけど、
その名前にすると、自動で読み込まれてしまう。。

開発用と本番用で切り替えたいときなどもあるので、@nuxtjs/pwaが生成するsw.jsと統合できるように、
`sw-firebase-messaging.js`という名前でファイルを作成しておく。

ファイル名を変更したので、上で書いている「firebaseの初期化」の部分で、
`messaging.useServiceWorker(registration)`を呼んでいる形。

```javascript
// ~/static/sw-firebase-messaging.js
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.14.2/firebase-messaging.js");

// Firebaseの初期化
firebase.initializeApp({
  apiKey: "...",
  authDomain: "...",
  databaseURL: "...",
  projectId: "...",
  storageBucket: "...",
  messagingSenderId: "...",
  appId: "...",
  measurementId: "...",
});

// [START background_handler]
const isSupported = firebase.messaging.isSupported();
if (!!isSupported) {
  const messaging = firebase.messaging();
  // バックグラウンド時の処理
  messaging.setBackgroundMessageHandler(function(payload) {
  　// 受け取ったFCMの内容を取得
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
      body: payload.notification.body,
      icon: payload.notification.icon,
    };

    // 通知を作成する
    return self.registration.showNotification(notificationTitle, notificationOptions);
  });
}
// [END background_handler]
```

##### nuxt.config.tsでPWA関連の設定をする

作成した`sw-firebase-messaging.js`を取り込む設定と、
@nuxtjs/pwaが生成するmanifest.jsonに、gcm_sender_idを追加する設定を追加

```typescript
import { Configuration } from "@nuxt/types";

const config: Configuration = {
  // 略
  modules: [
    "@nuxtjs/pwa",
  ],
  
  workbox: {
    // sw-firebase-messaging.jsをimportするように追加
    importScripts: [
      "sw-firebase-messaging.js"
    ]
  },

  pwa: {
    manifest: {
      // manifest.jsonにgcm_sender_idを追加
      gcm_sender_id: process.env.MESSAGING_SENDER_ID || ""
    }
  },
};

export default config;
```

これでクライアント側はOK！

#### サーバ側: メッセージを送信する

メッセージの送信は、firebase-adminでできる。
・[アプリサーバーからの送信リクエストを作成する  |  Firebase](https://firebase.google.com/docs/cloud-messaging/send-message?hl=ja)

firestoreを利用しているので、Cloud Functionsのfirestoreトリガーを使い、
ドキュメントが追加されたら通知するようにしている例。

```typescript
import * as functions from "firebase-functions";
import admin from "../common/firebaseAdmin"; // 初期化済みのfirebase-admin

export default functions
  .firestore.document("ドキュメントのパス")
  .onCreate(async (snap, context) => {
    // getTokenで保存しておいたトークンを取得(中身は略)
    const token = getToken();

    // 通知の送信
    const title = "通知するタイトル";
    const body = "通知する本文";
    const icon = "通知で表示するアイコン画像のURL"
    const link = "通知をタップしたときに開くURL"
    await admin.messaging().send({
        // 送信先の端末のトークン
        token: token,
        // 通知する内容
        notification: {
          title: title,
          body: body
        },
        // Web Push向けの通知内容
        webpush: {
          notification: {
            icon: icon
          },
          fcmOptions: {
            link: link
          }
        }
    });
  });
```

送信はこれだけ！

### ほかの小ネタ

#### トークンを削除する

`firebase.messaging().deleteToken();`でトークンを無効化できる。

```typescript
// トークンの削除
public async deleteToken(user: User) {
  const isSupported = firebase.messaging.isSupported();
  if (isSupported) {
    const token = await firebase.messaging().getToken();
    await firebase.messaging().deleteToken(token);
  }
}
```

#### フォアグラウンドで通知を受け取ったときになにかする

`onMessage`を使うと、通知を受け取ったときに呼び出してくれる。

```typescript
firebase.messaging().onMessage(async (payload) => {
  // 受け取ったときの処理
});
```

#### トークンが変更されたときになにかする

`onTokenRefresh`を使うと、トークンが更新されたときに呼び出してくれる。  
新しいトークンは再度`getToken()`を呼ばないといけない。

```typescript
firebase.messaging().onTokenRefresh(async () => {
  // トークンが更新されたときの処理
});
```

#### 通知の許可状態を確認する

通知の状態は、`Notification.permission`で確認できるらしい。
・[Notification.permission - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/notification/permission)

```typescript
if (Notification.permission === "default") {
  // 確認(デフォルト)
} else if (Notification.permission === "granted") {
  // 許可
} else if (Notification.permission === "denied") {
  // 拒否
}
```

### 【PR】これをつかって、こんなのつくりました！

![スクリーンショット 2020-05-24 12.44.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/2f71c56a-0ab2-5910-f203-e1fca191ad69.png)

こんな通知を受け取れます！
<img width="359" alt="スクリーンショット_2020-05-24_13_34_17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/1d4037bf-e662-a15a-4567-91f845ef5142.png">

１週間でWebサービスを作るイベント [web1week](https://crieit.net/boards/web1week-202005)への投稿作品です！
よかったら、遊んでみてください(*´ω｀*)

■エアで投げ銭できるWebサービス「エア銭」
URL: https://air-money.netlify.app/

# 参考にしたサイトさま
- [FRESH! における Web プッシュ通知機能 〜実装編〜 | CyberAgent Developers Blog](https://developers.cyberagent.co.jp/blog/archives/9662/)
- [quickstart-js/firebase-messaging-sw.js at bcce38ebc1e5602560e2b76b20f19b7834b8279e · firebase/quickstart-js](https://github.com/firebase/quickstart-js/blob/bcce38ebc1e5602560e2b76b20f19b7834b8279e/messaging/firebase-messaging-sw.js#L15-L37)
- [Messaging | JavaScript SDK  |  Firebase](https://firebase.google.com/docs/reference/js/firebase.messaging.Messaging#deletetoken)
- [複数のデバイスにメッセージを送信する  |  Firebase](https://firebase.google.com/docs/cloud-messaging/js/send-multiple#%E3%83%88%E3%83%94%E3%83%83%E3%82%AF-http-post-%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88)
- [REST Resource: projects.messages  |  Firebase](https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushConfig)
- [アプリサーバーからの送信リクエストを作成する  |  Firebase](https://firebase.google.com/docs/cloud-messaging/send-message)
- [バックグラウンド アプリにテスト メッセージを送信する  |  Firebase](https://firebase.google.com/docs/cloud-messaging/js/first-message)
- [JavaScript Firebase Cloud Messaging クライアント アプリを設定する](https://firebase.google.com/docs/cloud-messaging/js/client)
- [Firebase Cloud Messagingで始めるWebプッシュ通知 - Qiita](https://qiita.com/ryo_hisano/items/1171beca22d5a04ed802)