service workerでPush通知
PCのブラウザにslackのようなPush通知を送りたい場合、SSRの構成であれば、アプリケーションサーバーとクライアントとプッシュサーバーがあれば実現できる。
プッシュサーバーはクライアントのブラウザごとに用意できるかどうかが異なり、Chrome Firefox Edgeなどが対応している。
Push通知を送るまでの流れとしては…
クライアント側でPush通知受け取りの許可がされた場合必要な情報をアプリケーションサーバーに送る。
アプリケーションサーバーでそれらの情報を保管しておく。
Push通知を送る際に、保管情報をもとに、プッシュサーバーへリクエストを送る。
プッシュサーバーからブラウザがPush通知を受け取る。
という形になる。
現在仕事でチャットシステムを開発しており、特定のユーザーに向けてメンションが送られた際、そのユーザーのデバイスにPush通知を送るようなシステムを開発したのでその手順を紹介する。
構成
サーバー
Express
SocketIO
Node.jsの環境でWebsocketの接続ができるようになるライブラリ
web-push
サーバーからクライアントアプリに向けPush通知を流してくれるNPMパッケージ
DB
MongoDB (CosmosDB)
クライアント
Vue.js
SocketIO-client
SocketIOへ接続するためのクライアント側のライブラリ
SocketIOとSocketIO-clientで統一したバージョン管理を行わないと動かない
Axios
HTTPリクエストはAxiosで行う
service workerの登録
システムをログイン制にし、チャットで他のユーザーへメンションを投げられるようにしてみる。
その際、メンションをserviceWorkerの機能を利用しログインユーザーの端末にPush通知を送れるようにしてみたい。
VueでPWAを導入するための手順はNuxtで開発しているのか、vue-cliで開発しているのかで異なる。今回はvue-cliを利用していたため、そちらの方法を紹介する。
code:cli
vue add @vue/pwa
を実行すれば既存のプロジェクトにpwaの機能を追加することができる。
追加されるファイルに、registerServiceWorker.jsがあるが、これはブラウザにserviceWorkerを登録するjavascriptファイルになる。
code:registerServiceWorker.js
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(${process.env.BASE_URL}service-worker.js, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}
それぞれのファンクションは登録時のステータスに応じて呼び出される。エラー時にどうしたいといったことはここでハンドルができる。
また、vueはdevelopmentモードでのserviceWorkerの実行を推奨していないため、実際の登録はプロダクションで行う必要もある。
https://gyazo.com/69e382508d9d8cfccc844983ff8424ff
serviceWorkerがちゃんと登録されているかどうかは開発者ツールのApplicationタブをみて、Sourceにファイルが登録されていればいい。
Push通知許可の実装
よくwebで見かけるPush通知を許可するかどうかのボタンを実装する。これが許可されるとブラウザごとに設定されているPushサーバーからエンドポイントとなるURLとキーが発行される。サーバー側はPush通知を送りたい際にそのエンドポイントに向けてキーと一緒にPush通知として表示する内容を送り付けることができる。
https://gyazo.com/f8383ebd51b176adf2b9ccb1575d8ac1
PushManager.getSubscription().then(subscription => {})でsubscriptionにnullが入っていた場合、そのユーザーはPush通知を許可してなく、Push通知を許可していた場合は、subscriptionに上で述べた情報が格納される。これらの情報はこのPush通知を許可したユーザーに対して、Push通知を送るためにアプリケーションサーバーで保管しておく必要がある。
code:registerServiceWorker.js
swRegistration.pushManager.getSubscription()
.then(function(subscription) {
// 通知が購読されたかどうか
isSubscribed = !(subscription === null);
// もし購読されていれば、アプリケーションサーバーへ購読者情報の登録
if (isSubscribed) updateSubscriptionOnServer(subscription);
});
ニュースサイトの場合はサイトに訪れられた瞬間にPush通知の許可を求めてもいいだろうが、今回のようなログイン制のシステムの場合ログイン後にPush通知の許可を求めるようにするとスムーズにユーザと認証情報の紐づけができる。
(まだログインすらしてないシステムからいきなりPush通知を求められても許可する人はいないだろう)
service workerの実装
イベントごとにコードを書くことになる。
code:sw.js
// push通知を受け取ったときの挙動
self.addEventListener("push", function(event) {
const data = JSON.parse(event.data.text());
const title = data.title;
// push通知のbody アイコンの情報を詰める。
const options = {
body: data.message, // 表示メッセージ
icon: "../thumbnail/pwa/android-chrome-192x192.png", // アイコン
badge: "../thumbnail/pwa/android-chrome-192x192.png", // バッチアイコン
};
event.waitUntil(self.registration.showNotification(title, options));
});
また、serviceWorkerにはPush通知をクリックした際の挙動を記すことができ、アプリサーバー→Pushイベント→クリックイベントとその内容を指定することもできる。
アプリケーションサーバーでPush通知の認証情報を受け取る
serviceWorkerで発行されるPush通知に必要な情報は以下のような情報である。
code: json
{
"endpoint":"endpointURL",
"p256dh":"...",
"auth":"..."
}
endpoint - Push通知を送るためのPushサーバーのURL
p256dh - ブラウザが発行した公開鍵
auth - ユーザーエージェントとサーバー側で共有される共有鍵生成を難化するための乱数
開発しているシステムはログイン制のシステムなのでこれらの情報とユーザー情報を紐づけて保管している。ユーザーは複数のブラウザでシステムを扱う可能性があるためユーザーと認証情報は1:多の関係になる。
WebPushを用いてPush通知を送る
アプリケーションサーバーからPush通知を送るにあたってNode.js環境で利用できるweb-pushを利用した。 サーバー側でもあらかじめ公開鍵と秘密鍵のセットを生成しておき、それをweb-pushのsetVapidDetailsでセットしておく。
次にクライアントから受け取ったauth、p256dhの鍵、endpointのURLをセットすればPush通知を実行できる。
code:server.js
const webpush = require('web-push');
// サーバー側の鍵情報を詰める
webpush.setVapidDetails(
'mailto:example@yourdomain.org',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// Push通知を送るクライアント側の情報を詰める
const pushSubscription = {
endpoint: '.....',
keys: {
auth: '.....',
p256dh: '.....'
}
};
// Push通知を送る
webpush.sendNotification(pushSubscription, 'Your Push Payload Text');
Push通知は通常のHTTPリクエスト同様endpointのURLからPush通知の実行結果をHTTPステータスコードで受け取れるため、エラーハンドリングなどは各エンドポイントの仕様を確認すればできる。
腹が立つのがこのあたりの仕様がまったくエンドポイントのドメインごとに異なり、いちいち確認しなければいけないところ。
ユーザーがPush通知許可を取りやめたときのHTTPステータスコード
chrome - 403
firefox - 410
最終的に
あとはチャットの通信にWebPushを送る関数を呼び出せばチャットとPush通知機能がつながることになる。メンション先のユーザーがPush通知を許可していればブラウザから通知が表示されるようになる。
実際に世界で最も優れたブラウザVivaldiが受け取ったメンションのスクショ
https://gyazo.com/da804bf7096b343a0f438d678ef05de7
参考
Push通知に関するクライアント側の設定や書き方を大いに参考にした。
serviceWorkerで用いれる機能やAPIはほとんどMozillaのページを参考にした。