Firebase Cloud Functionsを使ってWEBアプリケーションでプッシュ通知を実装してみた

# はじめに
AngularとFirebaseで作ったWEBアプリケーションにチャット機能がありました。
チャットのメッセージが追加するたびにチャットに属するユーザーに対して、プッシュ通知するように機能を追加した時のことをまとめてみました。
※Anuglarの技術に関しての話はありません。

# 実装イメージ
チャットのメッセージが追加されて、Firebase RealtimeDatabaseの更新をトリガーにFirebase Cloud Functionsでデバイスに向けて通知を送る処理をする。
クライアント側には前もってServiceWorkerを使って、通知が来たときの画面に表示する処理を登録しておくと、通知が表示される。
ちなみに、iosにはServiceWorkerが使えないので、対象はAndoroid端末及びPCのGoogleChromeに限る。
※ハイブリット(ionic等)で書いてあってもios用にコンパイルするためにMacが必要なのでiosは出来ないと判断。社内はWindows一択なので。。

# いざ、実装
## Service Worker(sw.js)を作成
クライアント側で、更新通知が来たときの画面に表示をする処理が必要なので 
src配下にsw.jsを新規作成

```js:src/sw.js
self.addEventListener('push', function(event) {
  console.log('Push message', event);
  // Firebase Cloud Functionsでの関数での通知のデータをpayloadに格納
  const payload = event.data.json(); // ペイロードをJSONにパース

  const title = payload.title;
  const body =  payload.body;
  const icon =  payload.icon;
 
  event.waitUntil(
    self.registration.showNotification(title, {
      body: body,
      icon: icon,
      tag: 'my-tag'
  }));

```

## buildした時にdistファイルにsw.jsが登録されるように.angular-cli.jsonに追記
```json:.angular-cli.json
{
  "apps": [
    {
      "root": "src",
      "outDir": "dist",
      "assets": [
        "assets",
        "favicon.ico",
        "sw.js" //ここに追記
      ],
}

```

## ServiceWorkerの登録

```TypeScript:src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { AngularFire } from 'angularfire2';

import * as firebase from 'firebase'; // firebase.messagingを使用

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})

export class AppComponent implements OnInit {
  
 constructor(af: AngularFire) {}
 
 ngOnInit() {
   const messaging = firebase.messaging();

   // Firebase Messagingオブジェクトを取得します。
   this.registrationServiceWorker(messaging);

 }
 
 /**
   * サービスワーカーの登録
   * @param messaging
   */
 registrationServiceWorker(messaging) {
   // Service Worker とプッシュ メッセージが現在のブラウザでサポートされているかどうかを確認し、
   // サポートされている場合は sw.js ファイルを登録します。
   if ('serviceWorker' in navigator && 'PushManager' in window) {
     console.log('Service Workerとプッシュ通知をサポートされています');
     navigator.serviceWorker.register('sw.js').then(registration => {
       console.log('service worker登録出来ました');
       // firebase messagingでservice workerを使うのに必要
       // 既存のサービスワーカーを指定する為
       messaging.useServiceWorker(registration);
       this.requestPermission(messaging);
     }).catch(err => {
       console.error('service workerの登録出来ません', err);
     });
   } else {
     console.warn('プッシュ通知はサポートされていません');
   }
 }

 /**
   * 通知の権限リクエスト   最初の通知の権限のポップアップが出てきて許可をクリック
   * 通知許可がでたら通知を送るためのトークンを取得してデータベースに登録
   * @param messaging
   */
  requestPermission(messaging) {
    messaging.requestPermission().then(() => {
      console.log('通知OK');
    }).then(() => {
      // トークンを取得してデータベースに登録
      this.updateFcmToken(messaging);
    }).catch(err => {
      console.log('通知NG', err);
    });
  }
 
}

/**
  * トークンをFirebase ReartimeDatabaseにアップデート
  *　@param messaging
  */
updateFcmToken(messaging) {
  const userId = "ユーザーユニークキー"
  let fcmToken;
  messaging.getToken().then(token => {
    fcmToken = token;
  }).then(() => {
   　this.af.database.list(`mst/users`).update(userId, {
      fcmToken: fcmToken
    }).catch(err => {
      console.error('ユーザー情報のデータベース更新に失敗しました。', err);
    });
  }).catch(err => {
    console.error('fcmTokenの取得に失敗しました。', err);
  });
}


```

## pusu通知用のデータベースを構築
これまで通り、チャットで使ってたデータベースとは別にプッシュ通知用のデータベースを作って、チャットのメーセージが更新されると、プッシュ通知用のデータベースも更新。

### Friebase RealtimeDatabaseの構造（push通知用）
```
pushNorification: {
  chat: {
    messeges: {
      messageユニークID①: {
        msg: 'push通知だよ',
        sendDate: '2017/10/24(火) 9時19分',
        userId ; '送信者のユニークID',
        userName: '送信者'
      },
      messageユニークID②: {
        msg: 'push通知だよ二度目の',
        sendDate: '2017/10/24(火) 10時19分',
        userId ; '送信者のユニークID',
        userName: '送信者'
      }
    },
    users: {
      ユーザーユニークID①: {
　　　   fcmToken: 'fcmTokenの文字列',
        userId: 'ユーザーユニークID①',
        name: 'ユーザ名①'
      },
      ユーザーユニークID②: {
　　　   fcmToken: 'fcmTokenの文字列',
        userId: 'ユーザーユニークID②',
        name: 'ユーザ名②'
      }
    }
  }
}
```

## Firebase Cloud Functions実装
プロジェクトにfunctionsを追加
詳しくは、https://firebase.google.com/docs/functions/get-started?hl=ja

先程のデータベースのpushNotification/chat/messagesの更新をトリガにークライアントに通知を飛ばす処理をfunctions/index.jsに書いて行きましょう。

```js:functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');

const msgRef = functions.database.ref(`pushNotification/chat/messages/{msgId}`);

// チャットのメッセージが更新されると実行する
exports.pushChatMsg = msgRef.onWrite(event => {
  const message = event.data;
  const sendUserId = message.child('userId').val();
  

  const usersRef = message.ref.parent;
  usersRef.once('value').then(snapshot => {
    const users = snapshot.val().users;
    for (key in users) {
      const userId = users[key].userId;
      const userName = users[key].userName;
      const fcmToken = users[key].fcmToken;

      const payload = JSON.stringify({
        title: '○○アプリケーションからのお知らせ',
        body:　userName + 'さんからのメッセージがあります。',
        icon: 'oo.storage.com',
        url: 'oooooooooo.com'
      });
      
      if ((fcmToken !== null) && (fcmToken !== ``)) {
        pushToDevice(fcmToken, payload);
        console.log('チャット通知:' userName);
      }
    }
  })
})

// 特定のfcmTokenに対してプッシュ通知を打つ
function pushToDevice(token, payload){
  console.log("pushToDevice:", token);

  // priorityをhighにしとくと通知打つのが早くなる
  const options = {
    priority: "high",
  };

  admin.messaging().sendToDevice(token, payload, options)
  .then(pushResponse => {
    console.log("Successfully sent message:", pushResponse);
  })
  .catch(error => {
    console.log("Error sending message:", error);
  });
}


```

## Firebase deploy
全てのソースコードが出来たら、Firebaseにデプロイします。
```
firebase deploy    // 全てデプロイ
firebase deploy --only hosting　// ホスティングだけデプロイ
firebase deploy --only functions　// functionsだけデプロイ
```

これで、一通りの準備は完了です。
あとはFirebaseのデータベースを更新してみましょう。