Edited at

マッチングアプリ強者を体験できる PWA 開発で、Web プッシュ通知を理解しよう

どうも、親からもらったお年玉でマッチングアプリを始めるも、いいね 0 件のまま 3 ヶ月で退会した@sadnessOjisan です。

今日はタイトルの通り、マッチングアプリ強者を体験できる PWA を開発します。具体的には、いいね 通知がたくさん届くアプリを開発します。なおこのアプリは「通知止まらん www」を体験できるアプリを作ったの影響を深く受けました。ただし、これから作る push 通知体験アプリでは、通知の疑似体験ではなく本当に通知が届きます

moreline.gif

シンプルにプッシュを送るだけのコード: https://github.com/sadnessOjisan/simple-push

マッチングアプリ っぽいUIがついたコード: https://github.com/sadnessOjisan/simple-push-ui


書いた動機

この記事では Firebase を使った web push 通知を行う方法について、なるべく周辺知識に触れながら、解説を行っていきます。はじめて push 通知を行いたい人が、全体感を掴むと同時に、実際に手を動かして push 通知を送信できるようになる記事を目指しています。

どうしてこの記事を書いているかで言うと、いま現在(2019 年 7 月)、インターネット上に転がっている情報を集めながら開発していると、web push 通知機能について混乱を引き起こす可能性があり、情報を整理しておきたかったからです。その混乱の理由としては次の 3 つが挙げられます。


  • いまはサポートされていないサービスや規格について言及している記事の SEO パワーが強い

  • push を送る方法が 1 通りではなく、誤った方法の組み合わせをしてしまう可能性がある

  • push を行えるサービス(たとえば Firebase)の概念なのか、web push 通知そのものに関する概念のものかが分からない


    • ex) VAPID, FCM, GCM, gcm_sender_id, APNs, Push Server, Push Service など



ちなみに私は、はじめてweb push通知機能を実装する際、「web push 通知」でググったページだけで開発を試みましたが、動かなかったです。その後に Firebase のチュートリアルも行いましたが、自分で push 通知を送ることはできなかったです。

そこでこの記事では、push 通知について解説し、Firebase をどう使えば良いかを、なるべく写真やコードを示しながら、手を動かして学べるようにしました。


push 通知の仕組み

そもそも push 通知ってなんでしょうか?

スマホやブラウザを操作していると、上からひょこってなんか出てきますよね。それが push 通知でしょうか。

半分合っていて、半分間違っています。それはプッシュ通知そのものではなく、ただの通知とも呼べるからです。

なぜならサーバーから Push しなくても通知は出せるからです。たとえばアラーム機能などがそうです。

alarm.jpg

そのため、push 通知機能は、


  • サーバーから push された通知を受け取る機能

  • その受け取った通知をユーザーに表出させる機能

からなると捉えると良いでしょう。

この感覚は、後々デバッグするときに知っておくと役に立ちます。

たとえば、サーバーからの push に失敗しているのか、通知に失敗しているのかの切り分けができるようになります。

また切り分け観点では、iOS/Android アプリに通知を出すことと、ブラウザに通知を出すことも全然違うことを意識しておいてください。規格や利用できるツールからして大きく異なります。そのためweb push通知について調べる際はiOSやAndroidの情報に気をつけてください。(「push通知」ではなく「web push通知」と言う言葉で検索すると良いと思います。) 1

そこで次の節からは、iOS/Android に送る push 通知とブラウザに送る web push 通知について解説します。


iOS/Android にどう送るか

iOS/Android への push 通知は、この記事で主として取り扱いたい内容ではないので、プッシュ通知の基礎知識&秒間 1 万を超えるプッシュ通知基盤のアーキテクチャと仕組みとは (1/2)をまるっと引用した文章で説明します。


プッシュ通知は iOS デバイスに対してはアップルの「APNs(Apple Push Notification Service)」、Android デバイスに対してはグーグルの「GCM(Google Cloud Messaging)」を利用して実現します。

細かな違いはあるものの APNs・GCM 共に基本的な考え方は同じで、プッシュ通知を実現するためには以下の 2 つの機能を保つ必要があります。

デバイス情報の登録機能

プッシュ通知の配信機能

プッシュ通知は APNs・GCM に対して「対象アプリ」と「対象デバイス」を指定してメッセージを送る必要があります。「アプリ」については APNs・GCM からアプリごとに認証キーが発行されるため、それを保持しておけばよいのですが、問題は「デバイス」です。

デバイスを指定するためにはデバイストークン(GCM の場合は登録 ID に当たりますがデバイストークンに用語を統一)という情報を必要とします。デバイストークンはデバイス内でアプリから APNs・GCM に通信をしないと取得できません。

プッシュ通知を行うためにはデバイス情報を自分たちで管理する必要があるため、アプリ内でデバイストークンを取得した後に自分たちのサーバーに送信して格納しておく必要があります

プッシュ通知を配信するには、APNs・GCM に対して基本的には「認証キー」「デバイストークン」「メッセージ」を送る必要があります。これによりデバイストークンとひも付いた実端末に配信が行われます


ここで注目したい言葉は APNsGCM(いまは FCM に統一されています。) です。これらはそれぞれ、Apple と Google が管理している push 通知のためのサービスです。

ちなみに FCM のようなサービスを提供しているプラットフォームは Firebase だけではありません。いわゆる (m)BaaS を提供しているサービスであればサポートしているところもあるでしょう。たとえば、Azure にはNotification Hubsがあり、国内においても nifty がニフクラ mobile backendを提供しています。

また、これらのサービスに頼らずとも、APNs や GCM をそのまま使ったりもできます。その場合は自力で push 通知配信基盤を開発することとなります。メルカリ社は Gaurun といったライブラリを提供しており、配信基盤の開発を行うことができます。


web push 通知の仕組み

次に web push 通知を出す方法を解説します。

web push 通知は、 iOS/Android に対する通知と比べ、規格からまったく異なります。

web push 通知を行う際には、APNs や FCM といったサービスは不要です。ではどのようにして push 通知をしているのでしょうか。

それはブラウザそのものに web push を受け取れる機能を用意し、各ブラウザベンダーが提供している push server(push service とも呼ばれている)が push 通知をブラウザに送ります。そして、そのブラウザに備わっている Service Worker が、web push 通知をユーザーに通知しています。この仕組みは今の所、Google Chrome, Edge, Firefox, Android Chrome が対応しています。

web push 通知は、


  1. push server を発火させる

  2. pushserver が Service Worker にメッセージを送る

  3. Service Worker が通知を出す

といった順序で行われます。

そしてこれも混乱の原因なのですが、ここでいう push server は Firebase のようなサービスをさすのではなく、各ブラウザベンダーが提供している push 用のサーバーやサービスを差します。 そのような push server を利用するための方法や規格として Web Push protocol があります。この規格に則って、その push server を利用すれば push 通知は送れます。また、web-pushのようなライブラリを利用すると、この規格に沿った開発も比較的簡単に行えます。ただ、この方法は各ブラウザベンダーごとに実装を作る必要があり、少々苦労があります。

そこで、最近は Mozilla が採用していた VAPID (Voluntary Application Server Identification for Web Push) という仕組みを各ブラウザベンダーが採用したことを受け、この VAPID に則った実装によって全ブラウザ共通の通知機能を開発します。

VAPID はアプリケーションサーバーが作った JWT を Push サーバーが検証し、検証に成功したら Push を送る仕組みのことです。この仕組みによりシンプルにさまざまなブラウザーに web push 通知を送れるようになりました。

さらには Firebase (FCM) がこの VAPID という仕組みに対応したことで、FCM を利用してサポートブラウザすべてに通知を送れるようになりました。これまでは FCM 経由で、Google Chrome に push を通知するためには gcm_sender_id というものをアプリケーション(manifest.json)で明示する必要がありましたが、VAPID を使える今は不要になりました。(ただし古いブラウザー(v51 未満)をサポートする場合は必要です。)

Firebase が VAPID に対応したことにより、iOS や Android だけでなく Web にも通知を送れるようになりました。そのような背景があり、push 通知 = Firebase のような認識も持たれ始め、Firebase の用語と web push 通知の用語が混在して、自分が勉強したときに混乱したのだなと思っております。

次の章では、そんな便利な Firebase の使い方を紹介します。


Firebase から web push 通知を送ってみよう


Firebase を設定する

Firebase の初期設定を行います。

まずプロジェクトを作成しましょう。

projects.png

Firebase を利用するには、Firebase から提供されている SDK が必要です。

その SDK には config が必要であり、ここではその設定情報を取得します。

取得しないといけない設定項目は、

const firebaseConfig = {

apiKey: FIREBASE_API_KEY,
authDomain: `${FIREBASE_PROJECT_ID}.firebaseapp.com`,
projectId: FIREBASE_PROJECT_ID,
messagingSenderId: FIREBASE_SENDER_ID
};

です。すべて Settings に入っています。

toSetting.png

FIREBASE_API_KEY は「ウェブ API キー」、FIREBASE_PROJECT_ID は「プロジェクト ID

」、FIREBASE_SENDER_ID はクラウドメッセージングタブの「送信者 ID」です。これらはどこかに控えておきましょう。

general.png

fcm.png

また、クラウドメッセージングタブにある、ウェブプッシュ証明書から鍵ペアを発行し、これも保存しておいてください。これは VAPID キー というもので、実際に通知を送るときに必要になるものです。

さきの VAPID の説明に当てはめるなら、JWT の検証に必要となる公開鍵です。

vapid.png

ここで気をつけたいことは、これから Firebase の Firebase Cloud Messaging というサービスを利用するわけですが、サイドバーにあるそれは利用しません。

sidebar.png

このページに遷移すると Cloud Messaging 機能について解説されているのですが、web push 通知への導線がありません。結論から言うと、このサービスは今回は使わなくてもいいのですが、久しぶりに Firebase を使う場合などに混乱するかもしれないので気をつけておきましょう。

messaging.png


Service Worker を準備する。

web push 通知では、通知の受け取りは Service Worker 経由で行います。その Service Worker を設定しましょう。

firebase-messaging-sw.js という名のファイルをプロジェクトルートに設置しましょう。

そしてこれを JS で、navigator.serviceWorker.register('path/to/file')を実行し、Service Worker として登録します。

と言いたいところなんですが、Firebase SDK を利用する場合はアプリケーションのルートに firebase-messaging-sw.js を設置しておくと、Firebase SDK (firebase-messaging.js)がこのファイルを自動的に読み取り、Service Worker として登録してくれます。2

その、firebase-messaging.js の中身は次の通りです。ちょっとした解説やつまづきそうなポイントはコメントに書いています。

// importScriptsはservice worker内から他のserviceworkerを読み込むときに使えます

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: "11111111111"
});

const messaging = firebase.messaging();

/**
* background時の通知の扱い。ここではconsoleにメッセージを出力した上で、通知を出している。通知の中身はtitleやoptionから設定できる。
*/

messaging.setBackgroundMessageHandler(function(payload) {
console.log(
"[firebase-messaging-sw.js] Received background message ",
payload
);
var notificationTitle = "ONIAI";
var notificationOptions = {
body: "あなたのプロフィールがイイねされました",
// 通知の右にでる画像
icon:
"",
};

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

/**
* foreground時にメッセージを受け取ると、通知をする。通知の中身はtitleやoptionから設定できる。
*/

self.addEventListener("push", function(event) {
const title = "ONIAI";
const options = {
body: "あなたのプロフィールがイイねされました",
// 通知の右にでる画像
icon:
"",
// 通知の左にでる画像
badge: ""
};

event.waitUntil(self.registration.showNotification(title, options));
});

ここまでくれば、Service Worker から ブラウザに通知を送ることができます。

適当な HTML ファイルと一緒に Firebase Hosting などにアップロードして確かめてみましょう。[^2]

コンソールを開き、Application タブを選択してください。そこに Push とういボタンがあるはずです。

sw.png

それをクリックしましょう。おそらく通知が届いたかと思います。

sw.gif


push service から push 通知をブラウザに届ける

それでは Service Worker からではなく、push server 経由で通知を送ってみましょう。

まず Firebase の設定を行います。なお私の環境は TypeScript と webpack を利用して開発を行なっています。そのため Firebase の SDK は node module として DL しています。

そのため、webpack のようなモジュール間の依存解決を行う bunlder を利用しない場合は、CDN 経由で Firebase の SDK を読み込んでください。3

また、私は TypeScript で書いていますが、型注釈は入れていないので、コピペすれば普通の ES6 対応の JS 環境でも動くかと思います。

import * as firebase from "firebase/app";

import "firebase/messaging";

const FIREBASE_API_KEY = process.env.FIREBASE_API_KEY;
const FIREBASE_PROJECT_ID = process.env.FIREBASE_PROJECT_ID;
const FIREBASE_VAPID_KEY = process.env.FIREBASE_VAPID_KEY;
const FIREBASE_SENDER_ID = process.env.FIREBASE_SENDER_ID;

const firebaseConfig = {
apiKey: FIREBASE_API_KEY,
authDomain: `${FIREBASE_PROJECT_ID}.firebaseapp.com`,
projectId: FIREBASE_PROJECT_ID,
messagingSenderId: FIREBASE_SENDER_ID
};

firebase.initializeApp(firebaseConfig);

const messaging = firebase.messaging();

confing にあるものは冒頭でメモしてもらった値です。私は .env として外部に書き出しているため、このような書き方になっていますが、直接書いてしまっても問題はないです。(どうせコンソール開かれたら見えてしまうものです。)

これで Firebase Cloud Messaging (FCM) を使う準備が整いました。

そして次のコードをファイルに足しましょう。

messaging.usePublicVapidKey(FIREBASE_VAPID_KEY);

messaging.onMessage(payload => {
console.log("Message received. ", payload);
});

messaging
.getToken()
.then(currentToken => {
if (currentToken) {
document.getElementById("token").textContent = currentToken;
} else {
console.log(
"No Instance ID token available. Request permission to generate one."
);
}
})
.catch(err => {
console.log("An error occurred while retrieving token. ", err);
});

messaging.usePublicVapidKey(FIREBASE_VAPID_KEY); で VAPID で利用する公開鍵を登録しています。

そしてmessaging.onMessage()でメッセージを受け取ったときの挙動を書いています。ここではメッセージ内容をコンソールに出力しています。ただし、実際に通知を受け取ってからブラウザに表出させる処理は、さきほど Service Worker に書いているので、この console 出力だけでもきちんと通知されるようになっています。

また、web push 通知をデバイスに届けるためには、配信する際にどのデバイスへ送るかを指定する必要があります。

その識別には token と読んでいるものを利用します。

messaging.getToken() では、そのトークンを取得し、その続くチェーンで token を画面に表示させています。この token を push を送るスクリプトや関数に読み込ませることで、端末を指定して web push 通知を送ることができます。

それでは、通知を送ってみましょう。Firebase を push service として利用する VAPID 方式の web push 通知では、ヘッダーに認証キーをつけ、body で送り先の token を指定し、https://fcm.googleapis.com/fcm/send に POST するだけで通知できます。

だいたいこんな感じの POST を送れば良いでしょう。

curl -X POST -H "Authorization: key=${ここにFIREBASE_API_KEYがはいります}" -H "Content-Type: application/json" -d '{

"notification": {
"title": "ONIAI",
"body": "あなたのプロフィールがイイねされました",
"icon": "firebase-logo.png",
"click_action": "https://webpush2me.firebaseapp.com"
},
"to": "${ここにトークンが入ります}"
}'
https://fcm.googleapis.com/fcm/send

あとはこの curl を連打しまくれば、通知がたくさん来ます。これであなたもマッチングアプリ強者です。やったね。会議中のミラーリング画面にひょこっと通知させたり、スマホに送ってバイブ鳴らしましょう。

モブプロ中に通知が来るとだいたいこんな感じになります。

pc.gif

きっと周りのみんなは「えっ、あの人、モテモテなの!すごい」ってなります。

ちなみにモテてるように見える人は、本当にモテるようになるらしいです。


マッチングアプリっぽい UI にする

さあここまでで push 通知機能を作ることができました。しかし、push 通知が来た時に、「どんな子から来たの。見せてよ」って言われたらどうなるでしょうか。実際にリンクから飛ぶと index.html そのもののページに行ってしまいます。そこでこの章では UI を作っていきます。

この章で話したいことは、既存のプロジェクトでどのようにして web push 通知機能をいれるかです。

真面目な話、既存プロジェクトにプッシュ通知を導入するのは厄介な点があります。それは, manifest.json や firebase-message-sw.js やそれらが参照しているファイルは、ビルド後、デプロイ後の置き場所も決まっています。きちんとしたパスに置かないと、正しく動作しません。とくに JS をビルドするような開発方法をとっており、ビルド後スクリプトを別フォルダーに保存している場合にこの問題は起きます。

そこでここでは、React を使って UI を作り、そこに push を送る方法を解説します。とはいえ、難しいことは何も要求されません。ただ、ビルド時に必要なファイルをビルド先フォルダーへコピーしてあげるだけです。

いま、このような構造のディレクトリがあるとします。

$ tree -I node_modules -L 3

.
├── README.md
├── dist
│ ├── 7506011b33e7e3e37f329cc8c87ba57e.jpg
│ ├── build.js
│ ├── firebase-messaging-sw.js
│ ├── index.html
│ └── manifest.json
├── firebase.json
├── package.json
├── script
│ └── predeploy.sh
├── src
│ ├── assets
│ │ └── yk1.jpg
│ ├── component
│ │ └── Card.tsx
│ ├── firebase-messaging-sw.js
│ ├── index.d.ts
│ ├── index.html
│ ├── infra
│ │ ├── data.ts
│ │ └── firebase-messaging.ts
│ ├── main.tsx
│ ├── manifest.json
│ ├── page
│ │ └── Root.tsx
│ └── vendor
│ └── css
├── webpack.config.js
└── yarn.lock

src 配下は普通の React プロジェクトです。カードコンポーネントがあり、それが一人のプロフィールになっており、それを page/Root.tsx で並べています。また、firebase-messaging の設定は infra フォルダーに配置し、Root.tsx のマウント時で呼び出すようにしています。このプロジェクトは webpack で build すると、index.html は build.js を読み込む設定を加えられ、ビルド前のファイルは dist ディレクトリに build.js という名前で、共に吐き出されます。そしてこの dist ディレクトリをデプロイします。

その時に、index.html と build.js 以外に、push 通知に必要な manifest.json と firebase-messaging-sw.js もデプロイしないといけません。そのため、prebuild.sh という名前の build script を作成しました。

cp -r ./src/firebase-messaging-sw.js dist

cp -r ./src/manifest.json dist

もし、icon や badge の画像がある場合はそれも一緒にコピーしてあげましょう。

これを build 前に実行することで、FCM が必要とするファイルを dist フォルダーにコピーしましょう。

build コマンド は npm scripts で定義しています。

"build:prd": "BUILD_MODE='production' REACT_APP_ENV=production webpack",

buildコマンドの前に prebuild script を実行することでFCM をデプロイ先でも利用できるようになります。

"deploy:prd": "sh ./script/predeploy.sh & yarn run build:prd & firebase deploy"

これで build 処理があるようなプロジェクトでも push を受け取ることができるようになりました。

mn.gif


おまけ


登場人物の整理

言葉
説明

Firebase
Google が提供する mobile Backend as a Service (mBaaS)。認証やストレージなどを提供しており、その機能の一つとしてメッセージ通知サービス (FCM)がある。

FCM
Firebase Cloud Messaging。プッシュ通知を配信できる、Google 傘下のサービス。Android 端末だけなく、APNs 経由で iOS にも通知できる。iOS/Android 端末だけでなく Web にも通知を送れる(VAPID に対応したため)。

GCM
FCM と同じくプッシュ通知を配信できるサービス。 FCM の台頭により廃止された。

APNs
Apple が提供する プッシュ通知サービス。iOS に対して Push を行うことができる。

push server
各ブラウザベンダが保有している push 配信を行うためのサーバーやサービスを指す。例えば Mozilla は Mozilla’s Push Service を持っていたり、Edge は WNS を持っている。

VAPID
Voluntary Application Server Identification for Web Push のこと。PUSH の送信を行うための規格。アプリケーションサーバーが作った JWT を、Push サーバーが検証し、検証に成功したら Push を送る仕組み。

Service Worker
ブラウザが Web ページとは別にバックグラウンドで実行するスクリプト。push 通知文脈においては、メッセージの受信をユーザーに通知するために利用する。

Web Push protocol
web push を行うライブラリが従っている規格。push API が実装されているブラウザはこの規格に従うように作られている。


最後に一言

いいねの数だけで人の価値は決まりません。どれだけ 1 人の大切な人を想えるかが重要なのだと思います。





  1. これは自分の勘違いですが、web push 通知の仕組みは、iOS/Android と同じ通知をブラウザと Service Worker で受け取るものだと思っていました。 



  2. FYI: Firebase の Cloud Messaging(FCM)と Cloud Functions で Web プッシュ通知を行う 



  3. <script defer src="https://www.gstatic.com/firebasejs/5.10.1/firebase-app.js"></script> <script defer src="https://www.gstatic.com/firebasejs/5.10.1/firebase-messaging.js"></script> のようなものを html に書けば動くはずです。(構成の組み方は公式チュートリアルにもあるためそちらを参照してください。)