Angular5でのPush通知実装(firebaseあり版となし版)

WebPushを利用すると、WebアプリケーションでもPush通知をとばすことができます。
Angularでwebpushを実装する方法をまとめた記事になります。
firebaseを利用した場合と利用いない場合との両方をまとめています。
Angularは5になりますが、6な方も参考にできると思います。

## WebPushとは
VAPIDという鍵認証の仕組みを用いることで、Push通知を実現しています。いまいまの対応ブラウザはChrome、Firefox、Operaらしいです。
登場人物は主にクライアントアプリ、アプリサーバ、プッシュサーバの3つです。
簡単に説明すると下記の図のような流れで行われます。
(赤い四角は自分で用意しないといけないところで、黒い四角は自分で用意する必要がないところです)
![vapid.png](https://qiita-image-store.s3.amazonaws.com/0/85232/afa41555-b73e-d8e4-8cd1-d43d9e55e350.png)

firebaseを利用する場合は、もう少しシンプルになります。
firebase利用しない場合、Push通知に必要な情報は、通知先エンドポイント、公開鍵、乱数の3つでしたが、firebaseを利用する場合はSDKが返してくれるTokenのみになります。
アプリサーバで管理する情報もこのTokenのみになります。
このTokenは、通知先エンドポイント、公開鍵、乱数の3つをもとに生成されたものなので、通知リクエスト時にfirebase側でよしなに特定できるようになっているんだと思います。

![firebase.png](https://qiita-image-store.s3.amazonaws.com/0/85232/34b1bda7-fcf4-41cc-c74c-01df8d755cf9.png)

Push通知に必要な情報(通知先エンドポイントやToken)はデバイスごとに発行されるので、サーバ側でデバイスごとにPush通知情報を管理し、それぞれのデバイスに対してPushリクエストを送ることになります。

こちらの記事を参考にさせていただきました。こちらの記事をよむとより詳細が書かれています。
https://qiita.com/tomoyukilabs/items/2ae4a0f708a1af75f13e
https://qiita.com/tomoyukilabs/items/9346eb44b5a48b294762

## Angular5での実装(firebaseなし版)
まずはfirebaseなし版です。
(なし版といっても、Chromeを使っている場合はpushサーバでFirebase Cloud Messagingが利用されています。)

### 環境
- node: 9.10.1
- angular: 5.0.0
- angular/service-worker: 5.2.11(5.0.0だと動かないところがあったのでこれだけバージョンが違っています)
- PC: Windows10 Chrome 68
- モバイル: Android7 Chrome 68

### ServiceWorkerの追加
こちらを参考にAngularプロジェクトにServiceWorkerを追加します。
https://v5.angular.io/guide/service-worker-getting-started

この方法だと、Angularが用意しているServiceWorkerをそのまま利用するので、自分でServiceWorkerを実装する必要がありません。実際のコードを確認したい場合は、一度ビルドしてはきだされたものを確認するか、node_moduleの@angular/service-workerにあるので、そちらを確認してください。

紹介したリンク通りに進めると、prodオプション付きでビルドしたときのみ、ServiceWorkerファイルが生成されます。開発時にも有効にしたい場合は、app.module.tsでServiceWorkerWorkerModuleを登録するときにenabledオプションを指定しないでください。


### サーバ側の鍵ペアを生成
[web-push](https://github.com/web-push-libs/web-push)というpackageを利用して鍵を用意しました。
Push通知リクエストもこのpackageを利用すると容易に行えます。

```bash
npm install web-push --save
```

```js:generateKey.js
const webpush = require('web-push');

const vapidKeys = webpush.generateVAPIDKeys();

console.log('Public Key: ', vapidKeys.publicKey);
console.log('Private Key: ', vapidKeys.privateKey);
```

```bash
node generateKey.js
```

ここでコンソールにはかれた鍵を保管しておきます。

### Push通知情報の取得
Push通知を開始させるcomponentのngOnInitなどでPush通知情報を取得します。
試したときには、app.componentに追加しています。

まず、Angularが提供しているPush通知用のmoduleをinjectします。

```ts:app.component.ts
// ...省略
import { SwPush } from '@angular/service-worker';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
  constructor(
    private swPush: SwPush,
    // ...省略
  ) { }
}

```

そして、下記のコードを追加します。
Push通知許可のリクエストと、それが許可されたときにPush通知情報の取得を`this.swPush.requestSubscription`でやってくれます。

```ts:app.component.ts
  // ...省略
  ngOnInit() {
    this.subscribeToNotifications();
  }

  private subscribeToNotifications() {
    const VAPID_PUBLIC_KEY = 'xxxxxxxxxx'; // 先ほど生成したpublic keyを指定
    this.swPush.requestSubscription({ serverPublicKey: VAPID_PUBLIC_KEY })
      .then(sub => {
        console.log('sub: ', sub);
        console.log('endpoint: ', sub.endpoint);
        console.log('auth: ', this.arrayBufferToBase64(sub.getKey('auth')));
        console.log('p256dh: ', this.arrayBufferToBase64(sub.getKey('p256dh')));
      })
      .catch(err => console.error('Could not subscribe to notifications', err));
  }

  private arrayBufferToBase64(arrayBuffer) {
    const binary = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
    return window.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_');
  }
  // ...省略
```

実際は、ここで取得したエンドポイントなどのPush通知の情報をAPI等でサーバ側に送ることになるんですが、面倒だったのでとりあえずconsoleに出力しています。
これでクライアントアプリ側の準備はできたので、実際にChromeで動かして、通知を許可してみましょう。
consoleにendpoint, auth, p256dhが表示されるはずです。


### Push通知リクエスト
[web-push](https://github.com/web-push-libs/web-push)を使って、Push通知リクエストを送ります。web-pushがJWTの生成などをやってくれるので、こちら側でやることは簡単です。

下記のスクリプトを用意して、実行すればブラウザにPush通知が送られるます。

```js:webpush.js
'use strict';

const webpush = require('web-push');

const vapidKeys = {
  "publicKey": "",  // 最初に生成した公開鍵を指定
  "privateKey": ""  // 最初に生成した秘密鍵を指定
}

webpush.setVapidDetails(
  'mailto:',  // mailto:自分のメールアドレス（mailto:のままでも大丈夫）
  vapidKeys.publicKey,
  vapidKeys.privateKey
);

const subscription = {
  endpoint: '',   // クライアントアプリのconsoleに表示したendpointを指定
  keys: {
    auth: '',     // クライアントアプリのconsoleに表示したauthを指定
    p256dh: ''    // クライアントアプリのconsoleに表示したp256dhを指定
  }
}

const payload = {
  notification: {
    title: 'Push通知テスト',
    body: 'Push通知のテストですよ'
  }
};

webpush.sendNotification(subscription, JSON.stringify(payload))
  .then(res => console.log('push success: ', res))
  .catch(err => console.error('push failed: ', err));
```

webpush.jsを実行

```bash
node webpush.js
```

これでPush通知情報を取得した端末で、Push通知が届いているはずです！


## Angular5での実装(firebaseあり版)
※無料ですべてやりきれます。
次にfirebaseあり版です。こちらは鍵の用意などはfirebaseに任せればいいので、自分で管理する必要がありません。
ただ、ServiceWorkerはAngularのものをそのまま利用できないので、自分で用意しています。（なのでangular/service-workerは不要)

### 環境
- node: 9.10.1
- angular: 5.0.0
- firebase SDK: 5.3.1
- PC: Windows10 Chrome 68
- モバイル: Android7 Chrome 68

### firebaseでプロジェクト作成とプロジェクトのconfig取得
firebaseでプロジェクトを作成し、プロジェクトのconfigをメモっておきます。
プロジェクトのconfigは`Project Overview`の`ウェブアプリにFirebaseを追加`から確認することができます。
こんなやつです↓。あとから使うので、コピペしてどこかに保管しておいてください。

```js
  var config = {
    apiKey: "xxxxxxxxxx",
    authDomain: "xxxxxxxxxx.firebaseapp.com",
    databaseURL: "https://xxxxxxxxxx.firebaseio.com",
    projectId: "xxxxx",
    storageBucket: "xxxxxxxxxx.appspot.com",
    messagingSenderId: "xxxxxxxxxx"
  };
```

### APIキー(サーバキー)取得
firebaseのwebコンソールにて、プロジェクト設定画面に遷移します。（Project Overviewの右側にある歯車アイコンからいけます）
クラウドメッセージングタブにて、サーバーキーが確認できるので、その値をコピペしてどこかに保管しておきます。

### manifest.jsonの用意
manifest.jsonをsrc配下に作成します。
ここで使うのは、gcm_sender_idのみなので、他の値はてきとうで大丈夫です。
gcm_sender_idの値は世界共通なので、ここで指定している値を利用してください。

```json:manifest.json
{
  "short_name": "Push Notification App",
  "name": "Push Notification App",
  "icons": [
    {
      "src": "assets/icon/icon.png",
      "type": "image/png",
      "sizes": "128x128"
    }
  ],
  "start_url": "index.html?launcher=true",
  "display": "standalone",
  "gcm_sender_id": "103953800507"
}
```

（.angular-cli.jsonにmanifest.jsonを追加するのを忘れずに）

```json:.angular-cli.json
{
  // ...省略
  "assets": [
    "assets",
    "favicon.ico",
    "manifest.json"
  ],
  // ...省略
}
```

### service workerの用意
src配下にServiceWorkerを用意します。

```js:firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/5.3.1/firebase-app.js');
importScripts('https://www.gstatic.com/firebasejs/5.3.1/firebase-messaging.js');

firebase.initializeApp({
  'messagingSenderId': 'xxxxxxxxxx'    // プロジェクトのconfigのmessagingSenderIdを指定
});

const messaging = firebase.messaging();

messaging.setBackgroundMessageHandler(function(payload) {
  console.log('[firebase-messaging-sw.js] Received background message ', payload);
  const data = payload.data;
  if (!data) { return; }
  const { title, body } = data;
  var notificationTitle = title;
  var notificationOptions = {
    body
  };

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

※setBackgroundMessageHandlerはクライアントアプリがバックグランドにいるときのみ動作します。なので、ブラウザ上でクライアントアプリを開いていている場合には動作しません。そういう場合にはServiceWorkerではなくAngular側の実装で、通知を表示させることができます。


（こちらも.angular-cli.jsonに追加するのを忘れずに）

```json:.angular-cli.json
{
  // ...省略
  "assets": [
    "assets",
    "favicon.ico",
    "manifest.json",
    "firebase-messaging-sw.js"
  ],
  // ...省略
}
```

### Push通知情報の取得
firebaseのSDKを利用してPush通知情報を取得します。

プロジェクトにSDKを追加します。

```bash
npm install firebase --save
```

firebaseのプロジェクト作成時に取得したconfigをここで利用します。
適当な場所で、firebaseの初期化処理をします。
ここではわけあってapp.module.tsでやりました。

```ts:app.module.ts
// ...省略
import * as firebase from 'firebase';

const config = {
  apiKey: "xxxxxxxxxx",
  authDomain: "xxxxxxxxxx.firebaseapp.com",
  databaseURL: "https://xxxxxxxxxx.firebaseio.com",
  projectId: "xxxxx",
  storageBucket: "xxxxxxxxxx.appspot.com",
  messagingSenderId: "xxxxxxxxxx"
};
// ...省略
```

Push通知を開始させるcomponentのngOnInitなどでPush通知情報を取得します。
firebaseなし版と同様に、app.componentに追加しています。
ServiceWorkerのファイル名がfirebase-messaging-sw.jsであれば、serviceWorkerの登録とmessaging.useServiceWorkerの処理は不要らしいですが、試してないので残しています。
this.messaging.getToken()した値が、Push通知時に必要な情報です。firebaseなし版と同様console出力するようにしています。

```ts:app.component.ts
// ...省略
import * as firebase from 'firebase';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {

  messaging = firebase.messaging();

  constructor() { }

  ngOnInit() {
    this.getPermission();
    this.receiveMessage();
  }

  getPermission() {
    navigator.serviceWorker.register('firebase-messaging-sw.js')
      .then((registration) => {
        this.messaging.useServiceWorker(registration);
      })
      .then(() => {
        this.messaging.requestPermission();
      })
      .then(() => {
        console.log('Notification permision granted.');
        return this.messaging.getToken();
      })
      .then(token => {
        console.log('token: ', token);
      })
      .catch(err => {
        console.log('Unable to get permission to notify.', err);
      });
  }

  receiveMessage() {
    this.messaging.onMessage(payload => {
      console.log('Message received. ', payload);
      navigator.serviceWorker.getRegistration('firebase-messaging-sw.js')
        .then((registration) => {
          if (!payload.data) { return; }
          const { title, body } = payload.data;
          const notificationTitle = title;
          const notificationOptions = {
            body: body
          };
          // クライアントアプリがアクティブな状態のときは、こっち側でPush通知を表示する。
          registration.showNotification(notificationTitle, notificationOptions);
        });
    });
  }
}

```

クライアントアプリが表示されているときはPush通知を表示する必要がなければ、receiveMessageの処理は不要になります。

### Push通知リクエスト
firebseあり版では、POSTリクエストするだけです。
curlのサンプルをのせておきます。
Authorizationのkeyにはfirebaseプロジェクト作成後に取得したサーバーキーを指定してください。
リクエストボディのtoには、クライアントアプリのconsoleに出力したtokenを指定してください。
注意点として、dataというkeyに通知時に表示される内容を含めないと、setBackgroundMessageHandlerは動作しません。
https://github.com/firebase/quickstart-js/issues/71

```bash
curl https://fcm.googleapis.com/fcm/send \
     -H "Content-Type: application/json" \
     -H "Authorization: key=xxxxxxxxxx" \
     -d '{ "data": { "title": "Push通知テスト", "body": "Push通知テストですよ" },"to" : "xxxxxxxxxx"}'
```

これを実行すると、tokenを取得した端末でPush通知が届いているはずです！

## まとめ
長くなってしまいましたが、まとめると↓こんな感じです。

- firebase利用したほうが、運用面で楽になりそう
- firebase利用したとしても無料だし、WebPushやるさいはfirebase利用するほうがよさそう
- deviceごとのPush通知情報はどちらにせよサーバ側で管理する必要がある。このへん楽にできるサービスないかなと思ってたらAWS SNS使えばだいぶ楽になりそうです！！

## 参考
こちらを参考にさせていただきました。
https://qiita.com/tomoyukilabs/items/2ae4a0f708a1af75f13e
https://qiita.com/tomoyukilabs/items/9346eb44b5a48b294762
https://angularfirebase.com/lessons/send-push-notifications-in-angular-with-firebase-cloud-messaging/
https://github.com/firebase/quickstart-js/issues/71