こんにちは、UXデザインセンターのsgi-changです。

今日はドライブレコーダーの続き…ではなく、Web Push通知について記事を書いてみました。

Web Push通知と聞いて、皆さんはどんな仕組みを思い浮かべますか?

Web Push通知と言えば、モバイルのネイティブアプリで受け取れるPush通知を、Webブラウザでも同様に受け取れる、

そんなモバイルライクなUXを提供するProgressive Web Apps(PWA、以下同)の機能の一つとされています。

今回は、Firebase Cloud Messaging(FCM、以下同)経由で、PWAとも関わりの深い、Push通知を受信するために必要な技術、

Service Workerや、Notification APIを使って、

Web Pushの受信時に、FirebaseのSDK(以下、SDK)任せにしないで、受信側で通知をコントロールすることにあえて挑戦してみました。(需要はあるのだろうか…)

実装にあたり、特にFCM回りの設定や、Service Worker内でのNotificationの記述に焦点をあてて

以下の流れで進めていきたいと思います。

・FCM、PWA、Service Worker、Notificationについて

・FCMのドキュメントを読んで気づいたこと

– 送信メッセージのタイプは一つじゃないよって話

-メッセージ送信方法も一つじゃないよって話

-フォアグラウンドとバックグラウンドで動作が違う!?

-じゃあ、受信側でコントロールしちゃおう

・実装編

・まとめ

・エピローグ

FCM、PWA、Service Worker、Notificationについて

Web Push通知を実装するにあたり、はずせない技術、機能について

以下に簡単に説明します。

詳細は各リンク先の公式ページをご参照下さい。

Firebase Cloud Messanging (FCM)って何?

公式ページによると、

Firebase Cloud Messaging(FCM)は、メッセージを無料で確実に配信するためのクロスプラットフォーム メッセージング ソリューションです。

とあります。

受信側がTokenをFCMに発行してもらって、そのTokenを用いて送信側がFCMを経由してPush通知を送るといった仕組みです。

私の中ではメッセージの中継局みたいなイメージ。

もちろん、クロスプラットフォームなのでWebのみならずAndroid、iOS、Unityに対応しています。

Progressive Web Apps(PWA)って何?

PWAは、モバイルのネイティブアプリ(※)と同等のUXを提供し、かつ高速・高信頼性の高いパフォーマンスを提供するWebアプリを指す。

ざっくり言えば、スマートフォンのアプリと同じ機能をもったWebページ、アプリのこと。

そこには高パフォーマンスだったり、Push通知だったり、インストール可能だったり、デスクトップで動作したりモバイルで動作したり、レスポンシブデザインだったり、色んな特徴が含まれる。

(PWAを調べていくとやたらUXUX向上という文字があちこちに見られ、UXについて意識させられる。)

ページ下のほうにプログレッシブWebアプリのチェックリストのリンクとかがある。

※ネイティブアプリ…AndroidやiOS等の環境に特化した専用のアプリ。

Service Workerって何?

Webページとは別にブラウザのバックグラウンドで実行するスクリプト。

独自のライフサイクルで動いていて、定期的なバックグラウンド同期や、バックグラウンド時のプッシュ通知など、リッチなオフラインUXを提供する。

ただし、DOMに直接アクセスできない、httpsでないといけない(localhostは除く)、情報を持続できない(その際はIndexedDB APIを利用)等条件あり。

使用するにはまずWebアプリ上でService Workerの登録を行う必要がある。

前述のPWAを実現するためには、なくてはならない技術です。

Notification APIって何?

ユーザーに表示するシステム通知を制御する仕組み。

Webページがアイドル状態でもバックグラウンドでも通知を表示することができる。

また通知をカスタマイズすることができる。

Service Worker内で使用。

今回、受信側でPush通知をコントロールするのに活用しました。

FCMのドキュメントを読んで気づいたこと

「エピローグ」で後述しますが、WebPushを実装するにあたり、

かなり遠回りをしてしまいFCMのドキュメントをちゃんと読んでいないことを後悔しました。

そこであらためて読んで気づいたことを記載します。

送信メッセージのタイプは一つじゃないよって話

送信メッセージのタイプは

「通知メッセージ」(または表示メッセージとも言うらしい)

「データメッセージ」

の二つの種類があって、ざっくり説明すると

通知メッセージ…SDKによって自動的に処理

データメッセージ…クライアントアプリによって処理

とあります。

この仕組みがちょっと曲者で、さらっと色々と書いてあるのですが、

実際に動かしてみるまで何のことやらさっぱり分からず…ちょっとはまってしまいました。

(あ、はまったの私だけ!?)

例えば、

クライアント アプリに代わって FCM で通知の表示を処理する場合は、通知メッセージを使用します。クライアント アプリでメッセージを処理する場合は、データ メッセージを使用します。

とか、

FCM は、オプションのデータ ペイロードを含む通知メッセージを送信できます。その場合は、FCM によって通知ペイロードの表示が処理され、クライアント アプリによってデータ ペイロードが処理されます。

とか、ドキュメントに記載されていますが、

フォアグランドやバックグラウンド時等、場合によって通知メッセージを使用したり、

データペイロードを使用したりよきにはからってくれるものだと解釈して、どちらも実装していました。

しかし、後述しますが、実際にはフォアグランドやバックグラウンド時で実装を分けないといけなかったり、

さらに、通知をクリックされたことをアプリ側で知りたい場合があったり、

通知メッセージでも受信した際にクリックされたことを簡単に知ることができると思っていて、クライアントアプリ側でかなり苦戦してしまいました。

メッセージ送信方法も一つじゃないよって話

こちらの公式ドキュメントに記載されていますが、

送信方法は、「Firebase コンソールを使用して通知メッセージを送信」以外に、

「HTTP v1 プロトコル通知オブジェクト」(恐らく一番メジャーな方法)

「以前のHTTP プロトコル通知ペイロード」(いわゆるPost、curlで確かめることができる)

「XMPP プロトコル通知ペイロード」(ちょっとよく分からない、今回は割愛)

もあって、もちろん、それぞれ送信する内容も変わってきます…

例えば、Push通知を受信したときに、ユーザーがクリックして開くWebページを指定するにしても、

「HTTP v1 プロトコル通知オブジェクト」だと、Web Pushなら

webpush.fcm_options.link = “https://hoge.jp”

となりますが、

「以前のHTTP プロトコル通知ペイロード」だと、click_actionに宛先を指定することになり、フィールド名というかパラメータ名が違っていたりします。

フォアグラウンドとバックグラウンドで動作が違う!?

こちらもちゃんと公式ドキュメントに記載されていますが、

メッセージの動作はアプリの

「フォアグラウンド」

「バックグラウンド」

「完全に閉じられているか」(以下、クローズと呼ぶことにします。)

の状態によって異なる、と言っています。

バックグラウンド、クローズ時はSDKによって、通知メッセージの通知ペイロードを解釈してPush通知が表示されるのですが、

受信側ではコントロールしづらいのと、

フォアグラウンド時は、自分でNotificationを使ってメッセージを表示したり処理しないといけなく

また、Notificationで表示する場合、通知ペイロードでもデータメッセージでもどちらも扱えるので、

バックグラウンド、クローズ時と差異が発生する可能性もあったり、

記述する箇所もアプリ側だったりService Worker側だったり、

両方に記述してみて、一度のPush送信で通知が2つ表示されてしまったり…この時点でかなり混乱していました。

じゃあ、受信側でコントロールしちゃおう

そこでたどり着いたのが、

Push通知の受信から通知の表示までを、フォアグランドだろうが、バックグラウンドだろうが、クローズ時だろうが、Service Workerでやってしまおうと思ったのです。

その方が、

  • 記述が一箇所で済む
  • 表示する内容も統一できる
  • クライアント側で好きにデザインできる
  • クリックする先(自分のURL)を送信側が知る必要もなくなる(これはかなり楽)
  • クリックしたかどうかも取得できる!(大事!)

我ながらいい方法だな、と思ったのでした。(ニヤリ)

ちなみにSDK任せにせずに、全部クライアントアプリで受信するために、

通知メッセージの通知ペイロードは送らずにデータメッセージのペイロードのみ送信側から送ることにしました。(これ、重要)

まあ、フォアグラウンド時は別にPush通知は出す必要ないよって場合、

それ以外では表示だけならSDKがよきにはからってくれるから楽だし、それでいいよって場合には少々面倒くさいので、

あまりおすすめはしません…

実装編

それでは実装してみようと思います。

実装するアプリは、

で作ったVueアプリを使って、

Clientアプリで何か発話されたら、HostアプリにPush通知が届くようにしてみます。

ここでは、FCMやService Worker内でのNotificationの記述のみに焦点をあてて進めていくので、

送信側やその他の箇所は割愛します。

詳しくは、
GitHub:
ソースコードはこちらで公開しています。
https://github.com/sgi-chang/useServiceWorkerWithNotification
を見て下さい。

受信側のFCM設定の箇所

クライアントアプリ内で以下を実装します。

  • メッセージング オブジェクトの取得
  • 通知の受信許可をリクエストする…一番最初に受信許可を聞く
  • 現在の登録トークンの取得
  • トークン更新のモニタリング…トークンが更新されたら呼ばれる

ソース内の「process.env.VUE_APP_FIREBASE_PUBLIC_VAPID_KEY」の箇所は、

ご自身の設定した鍵情報に置き換えてください。

詳細は以下のページをご参照下さい。

// メッセージング オブジェクトの取得
this.messaging = firebase.messaging();
// アプリにウェブ認証情報を設定する
this.messaging.usePublicVapidKey(process.env.VUE_APP_FIREBASE_PUBLIC_VAPID_KEY);
// 通知の受信許可をリクエストする
this.messaging.requestPermission().then(() => {
console.log('Notification permission granted.');
// 現在の登録トークンの取得
this.messaging.getToken().then((token) => {
this.sendTokenToServer(token);
console.log('token', token);
})
// トークン更新のモニタリング
this.messaging.onTokenRefresh(() => {
this.messaging.getToken().then((refreshedToken) => {
console.log('Token refreshed.', refreshedToken);
}).catch((err) => {
console.log('Unable to retrieve refreshed token ', err);
});
});
}).catch((err) => {
console.log('Unable to get permission to notify.', err);
});

以下、すべてService Workerで実装します。

ファイル名は「firebase-messaging-sw.js」でpublicフォルダの下に置きます。

ServiceWorker、受信の実装

FCMから通知を受信すると呼ばれます。

このタイミングでNotificationに値をつめたり、通知内容を実装します。

// Push通知を受け取ると呼ばれる
self.addEventListener('push', function (event) {
// メッセージを表示する
let name = ((((event || {}).data || {}).json() || {}).data || {}).name || 'ふくろう';
event.waitUntil(
self.registration.showNotification(`${name}さんから`, {
'body': 'メッセージが届いています。',
})
);
});

ServiceWorker、click検知

表示した通知がclickしたら呼ばれます。

ここでクリック時の処理を実装します。

// 表示したNotificationがclickしたら呼ばれる
self.addEventListener('notificationclick', function (event) {
event.notification.close();
// ページを表示する
focusWindow(event);
});

ServiceWorker、バックグラウンド時のwindow focus

フォアグラウンド、バックグラウンド、クローズに合わせてページの表示方法を変えます。

フォアグランド…そのまま

バックグラウンド…該当のWindow、またはタブに移動

クローズ…新しくWindowを開く

// フォアグラウンド、バックグラウンド、クローズ時に合わせて表示する
// フォアグラウンド…そのまま
// バックグラウンド…遷移する
// クローズ…新規に開く
function focusWindow(event) {
const myPage = self.location.origin;
const urlToOpen = new URL(myPage, self.location.origin).href;
const promiseChain = clients.matchAll({
type: 'window',
includeUncontrolled: true
})
.then((windowClients) => {
let matchingClient = null;
// ブラウザのタブ、windowを検索
for (let i = 0; i < windowClients.length; i++) {
const windowClient = windowClients[i];
// フォアグラウンド時
// 開いているページなのでそのまま
if (windowClient.url === urlToOpen) {
matchingClient = windowClient;
break;
}
}
// バックグラウンド時
// 該当のページに遷移する
if (matchingClient) {
return matchingClient.focus();
} else {
// クローズ時
// ページを新規に開く
return clients.openWindow(urlToOpen);
}
});
event.waitUntil(promiseChain);
}
// クローズ時
// ページを新規に開く
function openWindow(event) {
const myPage = self.location.origin;
const promiseChain = clients.openWindow(myPage);
event.waitUntil(promiseChain);
}

確認方法ですが、node.jsで npm run serveとかで立ち上げた状態だと、Service Workerが登録されず確認することはできません。

自分でhttpsサーバーを立ち上げればいいのですが、とりあえず手っ取り早い方法として、

Chrome拡張機能の「Web Server for Chrome」を使えば、すぐにWeb Pushを試すことができます。

詳細は下記のページ等をご参照下さい。

まとめ

  • 送信メッセージは「データメッセージ」のみで送る。
  • Push通知の受信から表示、画面遷移までService Workerで実装できる。
  • アプリ側は、Tokenの登録や認証情報の設定だけでよい。

これで、受信側で自由自在にPush通知を操れるようになり、

快適なUX向上に役に立つのではないかと思います。

エピローグ

WebPushの実装に取り組んですぐの段階で、FCMのドキュメントをあまり見ずに、

VueがPWAの色々な機能をサポートしていて、その中にPush通知も含まれていたので、

簡単に実装できると思って飛びついてしまい、気づいたときには何が何やら分からない状態になってしまいました。

そして結局、FCMのドキュメントや、Service Workerについて知ることになり、

受信側で通知を制御できるじゃん!と行きついたのでよかったものの、

何事も始めにちゃんと調べるべきですね…(反省)

色々調べている過程で、「簡単にPush通知できた」とかSDK前提で表示するものは結構あるんですが、

フォアグラウンド、バックグラウンド、クローズの処理の違いとか、

メッセージ送信、タイプの違いとかに合わせて記述しているサイトがあまりなかったので、

備忘録もかねて書いてみることにしました。

少しでもUX向上にお役に立てれば嬉しいです。

余談ですが、この記事の終盤の、「実装編」をパパっと仕上げるつもりでしたが、

通知は検知できているのに、何をやってもnotificationの表示ができずにはまってしまいました。

原因は、OS(Windows10)の設定のアプリの通知がオフになっていたことでした。

ずっとNotificationはブラウザの世界で完結しているものと思い込んでいたので、かなり衝撃でした。

まだまだ修行が足りません…

最後までお読み頂き、誠にありがとうございます。