NuxtとFirebaseでPush通知を送る

Nuxt✖️Firebase構成のWebアプリで、PWA（Android）のPush通知機能を実装する機会があったのでメモ
※iosは11.3 から PWA に対応はされましたが、iOS 13 の時点でもPush通知（とその他諸々）には対応していないのでAndroid向けとなっています

# 処理の流れ
チャットアプリのように誰かにメッセージを送ったときに、
同時にPush通知を送るようなものを前提としています

１　ユーザーAに通知の許可を得る（クライアント側）
２　通知用のtokenを取得する（クライアント側）
３　２で取得したtokenをユーザーAに紐付ける形で保存する。今回はFirestoreを使用(サーバー側)
４　ユーザーBがなんらかのアクションを起こした時に、ユーザーAのtokenに対してPush通知を送る(サーバー側)


# 実装

## firebaseを初期化
`~plugins/firebase.ts`を作成し、firebaseを初期化する。（ファイル名はなんでもOK）

```ts:firebase.ts
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/functions';
import 'firebase/storage';
import 'firebase/messaging';


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
});

// isSupportedでmessagingをサポートしているかを判別する。サポートしていないとエラーで動かない
if (firebase.messaging.isSupported()) {
  messaging = firebase.messaging();
  // 後で作成するsw-firebase-messaging.jsをsw.jsと統合するための設定
  navigator.serviceWorker
    .register('./sw.js') // sw.jsとは@nuxtjs/pwaが自動生成するファイル。これが Service Worker となる
    .then((registration) => {
      messaging.useServiceWorker(registration);
    })
    .catch((error) => {
      console.error(error);
    });
}

```


## tokenを取得・登録する
Push通知の宛先としてトークンを取得/登録する
トークンは端末ごと異なるものが発行されるので複数端末を考慮する場合は注意が必要

```ts:index.vue
import firebase from "~/plugins/firebase";


async getToken() {
  const isSupported = firebase.messaging.isSupported();
  if (!isSupported) return;
  return firebase.messaging().getToken();
}

async setToken(token: string)
  // firestoreにトークンを保存しておく処理。
}

```

getToken()の際にブラウザの通知設定が「確認（デフォルト）」だと通知許可を求めるダイアログが表示されるので許可する必要あり
ブラウザ通知設定によって処理分け等したい場合は
https://developer.mozilla.org/ja/docs/Web/API/Notification/permission
この辺が参考になるかと


## 独自のサービスワーカー設定

staticフォルダの下にのファイルを作成
ファイル名はなんでもOK。今回は`~static/sw-firebase-messaging.js`
firebase.initializeApp()にmessagingSenderIdを引数で渡す必要がある。
テストで作る場合などはハードコードでも良いかもですが、
個人、開発、本番等でfirebaseを使い分け異なるmessagingSenderIdをセットしたい場合には[後述](#環境によってセットするfirebaseの情報を変える場合)の手段を取っている

```js:sw-firebase-messaging.js
// firebaseのバージョンを書く。すでにfirebaseを導入している場合はそのバージョンを記述（例：6.6.2
const FIREBASE_VERSION = '〇〇';

importScripts(`https://www.gstatic.com/firebasejs/${FIREBASE_VERSION}/firebase-app.js`);
importScripts(`https://www.gstatic.com/firebasejs/${FIREBASE_VERSION}/firebase-messaging.js`);
// 環境によってinitializeApp() にセットするfirebase messagingSenderIdを分たい場合に必要
importScripts('/tenshokudoki/swenv.js');

if (!firebase.apps.length) {
  firebase.initializeApp(swEnv); // ハードコードでもOKであれば{ messagingSenderId: '〇〇' }を直でセット
}

if (firebase.messaging.isSupported()) {
  const messaging = firebase.messaging();
  messaging.setBackgroundMessageHandler(payload => {
    const notificationTitle = payload.notification.title;
    const notificationOptions = {
      body: payload.notification.body, // 通知の本文
      icon: payload.notification.icon // 通知に表示されるアイコン
    };

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

## nuxt.config.ts で諸々の設定

```ts:nuxt.config.ts

export default {
  modules: [
    "@nuxtjs/pwa",
  ],

  workbox: {
    // sw-firebase-messaging.jsをimportするように追加
    importScripts: [
      "sw-firebase-messaging.js"
    ]
  },

  pwa: {
    manifest: {
      〜略〜
      gcm_sender_id: process.env.messagingSenderId|| ""
    }
  },
}
```

## 環境によってセットするfirebaseの情報を変える場合
個人、開発、本番環境等でfirebaseを使い分ける場合の対応

modulesフォルダに任意の名前のファイルを作成。今回は`~modules/swEnvBuild.js`
実行する環境で使っているfirebase のmessagingSenderIdを取得し`./static/swenv.js`として書き出す処理

```js:swEnvBuild.js
const fs = require('fs');

const environment = process.env.NODE_ENV || 'development';
const property = require(`../../../src/env.${environment}.js`); // 環境によって取得する環境変数の値を変更する
const path = './static/swenv.js';
const data = `const swEnv = { messagingSenderId: '${property.firebase.messagingSenderId}' };`;
fs.writeFileSync(path, data);

```

実行環境のmessagingSenderIdを書き出す処理ファイルをビルド前に走らせて、
ビルド実行時にsw-firebase-messaging.jsのなかで値を参照できるようにする
※sw-firebase-messaging.js（というかstaticフォルダ内のファイル）はimportとかで環境変数のファイル読み込んだりできないため

```json:package.json
"scripts": {
  "prebuild": "node modules/swEnvBuild.js",

  略

```

## サーバー側でメッセージ送信する
Push通知の送信準備が整ったので、実際に通知を送る処理を書きます
cloudfunctionsを想定していますが、
APIとして登録する部分は省略し、通知を送るロジック部分のみ記述しています

```ts:message.ts
import * as admin from 'firebase-admin';

public async sendMessage(content) {
  try {
    await admin.messaging().send({
      // 送信先の端末のトークン
      token: content.token,
      // 通知する内容
      notification: {
        title: content.title,
        body: content.body,
      },
      // Web Push向けの通知内容
      webpush: {
        notification: {
          icon: content.iconUrl,
        },
        fcmOptions: {
          link: content.linkUrl,
        },
      },
    });
    return true;
    } catch (error) {
      this.logger.error('PUSH通知の送信に失敗しました', error);
      return false;
  }
}
```

## 終わり
環境変数周りは泥臭い実装になっています、、
他にもっといい方法があれば教えていただけますと、、！

最後までご覧いただきありがとうございました！