WebPush
VAPIDと呼ばれる仕組みを使ってWebPushを実装する。VAPIDの仕組みそのものは下記記事を参照。
https://qiita.com/tomoyukilabs/items/9346eb44b5a48b294762
処理の流れ
大雑把な流れは
- クライアントでアプリケーションサーバから公開鍵の取得
- サービスワーカーをブラウザへ登録
- アプリケーションサーバの公開鍵をPushサーバに登録して、クライアント公開鍵・クライアント秘密乱数・エンドポイントを取得
- クライアント公開鍵・クライアント秘密乱数・エンドポイントをサーバに登録
- アプリケーションサーバで任意のタイミングでエンドポイントに向けてリクエストを送信
- PushサーバからブラウザにPush送信
- サービスワーカーでpushイベントをハンドルして通知
設計
VAPIDでPushを行うに当たって、下記の値が必要になる。
- アプリケーションサーバ秘密鍵
- アプリケーションサーバ公開鍵
- クライアント公開鍵
- クライアント秘密乱数
- エンドポイント
上記のうち、サーバの鍵はコマンドで生成しておく。
$ openssl ecparam -genkey -name prime256v1 -out private_key.pem
$ openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr -d '=' |tr '/+' '_-' >> public_key.txt
$ openssl ec -in private_key.pem -outform DER|tail -c +8|head -c 32|base64|tr -d '=' |tr '/+' '_-' >> private_key.txt
テーブル作成
クライアント公開鍵、クライアント秘密乱数、エンドポイントの3つはクライアントを区別するために必要なため、DBに保存する必要がある。
アプリケーションのユーザIDと紐づけておく。
カラム名 | 型 | 説明 |
---|---|---|
id | int(11) | pkey |
user_id | int(11) | ユーザのID |
endpoint | varchar(2048) | エンドポイント |
key | varchar(255) | クライアント公開鍵 |
token | varchar(255) | クライアント秘密乱数 |
web-push-php
アプリケーションサーバからPushサーバへのリクエストはweb-push-libs/web-push-phpを使う。
$auth = array(
'VAPID' => array(
'subject' => 'https://hoge.com',
'publicKey' => $public_key,
'privateKey' => $private_key,
),
);
$webpush = new \Minishlink\WebPush\WebPush($auth);
$payload = [
'title' => '通知があります。',
'message' => $message,
'url' => 'https://hoge.com/user/'.$user_id
];
$res = $webpush->sendNotification(
$endpoint,
json_encode( $payload ), // jsonで送ると、pushイベントでjsonでデータを取得できる。
$key,
$token,
true
);
サービスワーカー
Pushサーバから送られたPushはサービスワーカーで処理する。
登録
サーバから公開鍵を取得して、サービスワーカーを登録する。また、サービスワーカーの登録に成功したら、Pushサーバからsubscriptionの取得を行う。取得できなかった場合はサーバ公開鍵と一緒に登録を行う。
var serverKey = urlB64ToUint8Array(data.result.server_key); // Util function from https://github.com/GoogleChrome/push-notifications/blob/master/app/scripts/main.js#L31-L44
navigator.serviceWorker.register('serviceworker.js', { scope: './' }).then(function (registration) {
// getSubscription()で登録済みのsubscriptionを取得してみる
registration.pushManager.getSubscription().then(function (subscription) {
if (subscription) {
registerSubscription(subscription);
return;
}
// 登録済みのsubscriptionがない場合は新たに登録する
registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: serverKey
}).then(function (subscription) {
// Pushサーバに登録できたらsubscriptionをアプリケーションサーバにも登録する
registerSubscription(subscription);
return;
})
});
});
イベントハンドリング
よく使うと思われるイベントは下記の2つ。
- pushイベント:送られてきたPushイベントをハンドルして、
registration.showNotification
で通知する - notificationclickイベント:通知をクリックしたときの処理を記述する
アプリケーションサーバで$payload
に入れてjsonで送ったデータはpushイベントのevent.data.json()
で取得できる。
また、showNotificationの第二引数に渡したオブジェクトは、notificationclickイベントのevent.notification
で取得する。
self.addEventListener('push', function(event) {
const data = event.data.json() // payloadで送ったデータの取得
return event.waitUntil(
self.registration.showNotification(
data.title,
{
// ここのデータをevent.notificationで取得する (event.notification.bodyとか)
icon: '/img/icon.png',
body: data.message,
data: {
url: data.url
}
}
)
);
}, false);
self.addEventListener('notificationclick', function(event) {
event.notification.close();
const data = event.notification.data;
event.waitUntil(clients.matchAll({
type: 'window'
}).then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
if (client.url === data.url && 'focus' in client) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(data.url); // $payloadで送ったurlを開く
}
}));
});