こんにちは!
最近めがねデビューした、入社2年目の茶谷です。
今回は、PHPとJavaScriptでWebPushに挑戦してみました!
WebPushとは
WebPushは、ウェブブラウザを通じてユーザーに対してプッシュ通知を送信するための仕組みで「Service Worker」、「Push API」、「Notification API」の3つから構成されています。
Service Worker(サービスワーカー)
WebPushの基盤となるもの。バックグラウンドで実行されるスクリプトで、ウェブページとは独立して動作する。サービスワーカーはプッシュ通知を受信し、PCやスマホなどのデバイスに通知を表示する役割を果たす。
Push API
サービスワーカーに対してプッシュ通知を受け取るためのメカニズムを提供する。WebアプリケーションはPush APIを使用してプッシュ通知を要求し、サーバーはそれを処理して通知を送信する。
Notification API
ウェブアプリケーションがユーザーに対して表示する通知を制御するためのもの。これを使用すると、プッシュ通知の外観や動作をカスタマイズできる。
Web Pushを使うための準備
Web Pushを使ってPush通知を実装するには、秘密鍵と公開鍵を生成する必要があります。
今回は https://web-push-codelab.glitch.me/ で作成しました。
もし自分で秘密鍵と公開鍵を生成する場合は、W3CのPushSubscriptions Interfaceの仕様を満たす鍵を生成する必要があります。
引用:
If present, the value of applicationServerKey MUST include a point on the P-256 elliptic curve [DSS], encoded in the uncompressed form described in [ANSI-X9-62] Annex A (that is, 65 octets, starting with an 0x04 octet). When provided as a DOMString, the value MUST be encoded using the base64url encoding [RFC7515].
7.2 PushSubscriptionOptions Interface
ChatGPTに翻訳させてみました。
applicationServerKey" の値が存在する場合、その値は [DSS] で定義された P-256 楕円曲線上のポイントを含まなければなりません。これは [ANSI-X9-62] の付録 A に記載されている非圧縮形式でエンコードされる必要があります(すなわち、0x04オクテットで始まり、65オクテット)。DOMString として提供される場合、その値は base64url エンコーディング [RFC7515] を使用してエンコードされる必要があります。
購読 ... ユーザーが特定のWebサイトやWebアプリケーションからプッシュ通知を受け取ることに同意するプロセスを指す
フロントエンドの実装
1. manifest.jsonの設置
今回は、PCだけでなく(iOS 16.4以降の)iPhoneでも通知されるようにします。
iPhoneでWebPush機能を使えるようにするには、PWA化が必須です。
PWAについてはわかりやすそうな記事を見つけたので下記を参照してください。
PWA(Progressive Web Apps)とは!PWAがどう凄いのかや導入するメリットを解説!
WebサービスをPWAに対応するには、manifest.jsonというファイルをドキュメントルートに配置します。
今回は下記のようなmanifestファイルを作成し、ドキュメントルートの直下に設置しました。
1 2 3 4 5 6 7 8 |
manifest.json { "name": "WebPusher", // アプリケーションの名前 "short_name": "WebPusher", // アプリケーションの短縮名 "display": "standalone", // アプリケーションがどのように表示されるか "scope": "/", // アプリケーションのスコープ。アプリケーションがどの範囲で動作するかを定義 "start_url": "/" // アプリケーションが最初に開かれるときの URL } |
iOS 16.4+でプッシュ通知を有効にするには、displayをstandaloneかfullscreenに設定することが必須だそうです。
作成したmanifestファイルを読み込むために、すべてのページで読み込まれるheader.htmlに以下を追記します。
1 |
<link rel="manifest" href="manifest.json"/> |
2. サービスワーカーの登録
WebPushを利用するには、サービスワーカーを登録する必要があるので、下記のようにして登録します。
ドキュメントルートディレクトリ直下にservce-worker.jsを作成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// バックエンドからプッシュ通知を受け取ったときの処理 self.addEventListener('push', function (event) { const title = 'Push通知テスト'; const options = { body: event.data.text(), // サーバーからのメッセージ tag: title, // プッシュ通知のタイトル icon: 'icon-512x512.png', // アイコンの画像 badge: 'icon-512x512.png' // バッジの画像 }; // showNotificationメソッドでプッシュ通知を表示 event.waitUntil(self.registration.showNotification(title, options)); }); // プッシュ通知をクリックしたときのイベント self.addEventListener('notificationclick', function (event) { event.notification.close(); // 閉じる // その他処理 }); // Service Worker インストール時に実行される self.addEventListener('install', (event) => { console.log('service worker install ...'); self.skipWaiting(); // アクティブなサービスワーカーの強制切り替え }); // Service Worker ソースの更新がある場合に実行される registration.addEventListener('updatefound', function() { console.log('service worker updating ...'); }); |
service-worker.jsをサービスワーカーに登録
1 2 3 |
if (window.navigator.serviceWorker !== undefined) { window.navigator.serviceWorker.register('/service-worker.js') } |
3. 購読処理の実装
Web Push購読に必要なことは次の3つになります。
- ユーザの利用している端末が、Web Push通知に対応しているかどうか確認
- バックエンド側の公開鍵を取得する
- (2.で取得した公開鍵を使って)Web Push購読
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
import api from '@/libs/api' // 1) ユーザの利用している端末が、Web Push通知に対応しているかどうか確認 export async function checkIsWebPushSupported() { // グローバル空間にNotificationがあればNotification APIに対応しているとみなす if (!('Notification' in window)) { return false } // グローバル変数navigatorにserviceWorkerプロパティがあればサービスワーカーに対応しているとみなす if (!('serviceWorker' in navigator)) { return false } try { const sw = await navigator.serviceWorker.ready // 利用可能になったサービスワーカーがpushManagerプロパティがあればPush APIに対応しているとみなす if (!('pushManager' in sw)) { return false } return true } catch (error) { return false } } // 2) バックエンド側の公開鍵を取得する export async function getVapidPublicKey() { let info await api.call('webpush_pubkey_get') .then(res => { info = res.data.info }) .catch(err => { console.log(err) }) return info } // 3) 2)で取得した公開鍵を使ってWeb Push購読 export async function subscribe() { if (!(await checkIsWebPushSupported())) { alert('ご利用のブラウザではWeb Pushは使えません') return false } const validPublicKey = (await getVapidPublicKey()).webpush_pubkey if (window.Notification.permission === 'default') { const result = await window.Notification.requestPermission() if (result === 'default') { alert('プッシュ通知の有効化がキャンセルされました。はじめからやり直してください。') return false } } if (window.Notification.permission === 'denied') { alert('プッシュ通知がブロックされています。ブラウザの設定から通知のブロックを解除してください。') return false } const hasSubscription = await getSubscription() if (hasSubscription) { // すでに購読している // 発行済みの購読情報を返す return hasSubscription.toJSON() } // 新しく購読情報を作成する const currentLocalSubscription = await navigator.serviceWorker.ready.then( (worker) => worker.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: validPublicKey, expirationTime: Math.floor(Date.now() / 1000) + 30 }) ) const subscriptionJSON = currentLocalSubscription.toJSON() if (subscriptionJSON.endpoint == null || subscriptionJSON.keys == null) { alert('ご利用のブラウザが発行したトークンは未対応のため、プッシュ通知はご利用いただけません。') return false } return subscriptionJSON } // すでに購読情報があるかチェック export async function getSubscription() { let subscriptionData = null await navigator.serviceWorker.ready.then(async function(serviceWorkerRegistration) { await serviceWorkerRegistration.pushManager.getSubscription() .then((subscription) => { subscriptionData = subscription }) .catch(function(err) { console.warn('Error during getSubscription()', err) }) }) return subscriptionData } export default { subscribe, getSubscription } |
今回は、GrpMailの個人設定 > 通知設定からWebPush機能を購読する想定で、上記ソースコード内のsubscribe()を、通知設定の保存時に実行させています。
正常に購読できると、下記のように購読情報が返ってきます。
1 2 3 4 5 6 7 8 |
{ "endpoint": "https://fcm.googleapis.com/fcm/send/caxIV6AdyG0:AP...", "expirationTime": null, // 購読情報の有効期限(VAPIDかつChromeの場合は有効期限は常にnull) "keys": { "p256dh": "BOzeihdk_UXw...", "auth": "1JY_l-F2x..." } } |
この購読情報を、ユーザIDと紐づけてDBに保存します。
次回は、バックエンド側の実装を行っていきます!