まず、push メッセージを送信する権限をユーザーから取得します。その後、PushSubscription
を取得します。
これを行う JavaScript API は非常に簡単です。ロジックフローを順に説明します。
特徴検出
まず、現在のブラウザが実際にプッシュ メッセージをサポートしているかどうかを確認する必要があります。プッシュがサポートされているかどうかは、2 つの簡単なチェックで確認できます。
- navigator で serviceWorker を確認します。
- window で PushManager を確認します。
if (!('serviceWorker' in navigator)) {
// Service Worker isn't supported on this browser, disable or hide UI.
return;
}
if (!('PushManager' in window)) {
// Push isn't supported on this browser, disable or hide UI.
return;
}
サービス ワーカーとプッシュ メッセージの両方のブラウザ サポートは急速に拡大していますが、両方の機能を検出して段階的に拡張することをおすすめします。
Service Worker を登録する
機能検出により、サービス ワーカーと Push の両方がサポートされていることがわかります。次に、Service Worker を「登録」します。
Service Worker を登録すると、Service Worker ファイルの場所がブラウザに通知されます。このファイルは単なる JavaScript ですが、ブラウザはプッシュを含む Service Worker API への「アクセス権」を付与します。厳密に言えば、ブラウザは Service Worker 環境でファイルを実行します。
Service Worker を登録するには、navigator.serviceWorker.register()
を呼び出してファイルのパスを渡します。次に例を示します。
function registerServiceWorker() {
return navigator.serviceWorker
.register('/service-worker.js')
.then(function (registration) {
console.log('Service worker successfully registered.');
return registration;
})
.catch(function (err) {
console.error('Unable to register service worker.', err);
});
}
この関数は、Service Worker ファイルがあり、その保存場所をブラウザに伝えます。この場合、Service Worker のファイルは /service-worker.js
にあります。register()
を呼び出した後、ブラウザはバックグラウンドで次の手順を実行します。
Service Worker ファイルをダウンロードします。
JavaScript を実行します。
すべてが正しく実行され、エラーがなければ、
register()
によって返される Promise が解決されます。なんらかのエラーが発生すると、Promise は拒否されます。
register()
が拒否された場合は、Chrome DevTools で JavaScript のスペルミスやエラーがないか再確認します。
register()
が解決されると、ServiceWorkerRegistration
が返されます。この登録を使用して、PushManager API にアクセスします。
PushManager API ブラウザの互換性
権限のリクエスト
Service Worker を登録し、ユーザーを登録する準備が整いました。次に、push メッセージを送信する権限をユーザーから取得します。
権限を取得する API は比較的シンプルですが、最近、コールバックを受け取る API から Promise を返す API に変更されました。この場合の問題は、現在のブラウザで実装されている API のバージョンを判断できないため、両方を実装して両方を処理する必要があることです。
function askPermission() {
return new Promise(function (resolve, reject) {
const permissionResult = Notification.requestPermission(function (result) {
resolve(result);
});
if (permissionResult) {
permissionResult.then(resolve, reject);
}
}).then(function (permissionResult) {
if (permissionResult !== 'granted') {
throw new Error("We weren't granted permission.");
}
});
}
上記のコードでは、重要なコード スニペットは Notification.requestPermission()
の呼び出しです。このメソッドは、ユーザーにプロンプトを表示します。
ユーザーが許可プロンプトを操作して [許可]、[ブロック] を押すか、閉じた場合、結果が文字列('granted'
、'default'
、'denied'
)として返されます。
上記のサンプルコードでは、権限が付与されている場合は askPermission()
によって返された Promise が解決されます。それ以外の場合は、エラーをスローして Promise を拒否します。
対処が必要なエッジケースの 1 つは、ユーザーが [ブロック] ボタンをクリックした場合です。この場合、ウェブアプリはユーザーに再度権限をリクエストできなくなります。ユーザーは、設定パネルに埋め込まれている権限の状態を変更して、アプリの「ブロックを解除」する必要があります。ユーザーに権限をリクエストする方法とタイミングを慎重に検討してください。ユーザーが [ブロック] をクリックした場合、その決定を簡単に取り消すことはできません。
ただし、権限が要求される理由がわかっていれば、ほとんどのユーザーは権限を付与できます。
後ほど、いくつかの人気サイトがどのように権限をリクエストしているかを見てみましょう。
PushManager を使用してユーザーを登録する
Service Worker が登録され、権限が付与されたら、registration.pushManager.subscribe()
を呼び出してユーザーをサブスクライブできます。
function subscribeUserToPush() {
return navigator.serviceWorker
.register('/service-worker.js')
.then(function (registration) {
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
),
};
return registration.pushManager.subscribe(subscribeOptions);
})
.then(function (pushSubscription) {
console.log(
'Received PushSubscription: ',
JSON.stringify(pushSubscription),
);
return pushSubscription;
});
}
subscribe()
メソッドを呼び出す際は、必須パラメータと省略可能なパラメータの両方で構成される options オブジェクトを渡します。
渡せるすべてのオプションを見てみましょう。
userVisibleOnly オプション
push が初めてブラウザに追加された時点では、デベロッパーがプッシュ メッセージを送信できても、通知を表示できなくてもよいかどうかの不確実性がありました。これは、バックグラウンドで何かが起こったことをユーザーが認識しないため、サイレント プッシュと呼ばれます。
懸念事項は、デベロッパーがユーザーに気付かないうちに、ユーザーの位置情報を継続的に追跡するといった厄介なことをする可能性があることでした。
このシナリオを回避し、仕様作成者がこの機能を最適にサポートする方法を検討する時間を確保するため、userVisibleOnly
オプションが追加されました。true
の値を渡すことは、プッシュを受信するたびにウェブアプリが通知を表示する(サイレント プッシュなし)ことをブラウザと合意するシンボリックな行為です。
現時点では、true
の値を渡す必要があります。userVisibleOnly
キーを含めない場合や false
を渡さない場合は、次のエラーが発生します。
現在、Chrome でサポートされているのは、ユーザーに表示されるメッセージにつながる定期購入の Push API のみです。これを指定するには、代わりに pushManager.subscribe({userVisibleOnly: true})
を呼び出します。詳しくは、https://goo.gl/yqv4Q4 をご覧ください。
現在のところ、Chrome では包括的なサイレント プッシュは実装されないようです。代わりに、ウェブアプリの使用状況に基づいて一定数のサイレント プッシュ メッセージをウェブアプリに実行できるようにする予算 API の概念について、仕様の作成者が検討しています。
applicationServerKey オプション
前のセクションで「アプリケーション サーバーキー」について簡単に触れました。「アプリサーバー鍵」は、プッシュ サービスがユーザーを定期購読しているアプリを識別し、同じアプリがそのユーザーにメッセージを送信していることを確認するために使用されます。
アプリケーション サーバー鍵は、アプリケーションに固有の公開鍵と秘密鍵のペアです。秘密鍵はアプリケーションに対して秘密にする必要があり、公開鍵は自由に共有できます。
subscribe()
呼び出しに渡される applicationServerKey
オプションは、アプリのパブリック キーです。ブラウザは、ユーザーを登録するときにこれをプッシュ サービスに渡します。つまり、プッシュ サービスは、アプリケーションの公開鍵をユーザーの PushSubscription
に関連付けることができます。
以下の図は、これらの手順を示しています。
- ウェブアプリがブラウザに読み込まれ、
subscribe()
を呼び出して、公開アプリケーション サーバー鍵を渡します。 - ブラウザは、エンドポイントを生成し、このエンドポイントをアプリケーションの公開鍵に関連付けて、エンドポイントをブラウザに返すプッシュ サービスにネットワーク リクエストを送信します。
- ブラウザは、このエンドポイントを
PushSubscription
に追加します。これは、subscribe()
プロミスによって返されます。
後で push メッセージを送信するときは、アプリケーション サーバーの秘密鍵で署名された情報を含む Authorization ヘッダーを作成する必要があります。push サービスが push メッセージの送信リクエストを受信すると、リクエストを受信するエンドポイントにリンクされている公開鍵を検索して、この署名付き Authorization ヘッダーを検証できます。署名が有効な場合、プッシュ サービスは、一致する秘密鍵を持つアプリケーション サーバーから送信されたことを認識します。これは基本的に、他のユーザーがアプリケーションのユーザーにメッセージを送信できないようにするセキュリティ対策です。
厳密に言えば、applicationServerKey
は省略可能です。ただし、Chrome への最も簡単な実装ではこれが必要です。他のブラウザでは今後必要になる可能性があります。Firefox では省略可能です。
アプリケーション サーバー鍵の内容を定義する仕様は VAPID 仕様です。「アプリケーション サーバー鍵」や「VAPID 鍵」という用語を目にするたびに、これらは同じものであることを覚えておいてください。
アプリケーション サーバー鍵を作成する方法
アプリケーション サーバー鍵の公開鍵と非公開のセットを作成するには、web-push-codelab.glitch.me にアクセスするか、web-push コマンドラインを使用して鍵を生成します。手順は次のとおりです。
$ npm install -g web-push
$ web-push generate-vapid-keys
これらの鍵は、アプリに対して 1 回だけ作成する必要があります。秘密鍵は非公開にしてください。(そう、今言ったばかりです)。
権限と subscribe()
subscribe()
を呼び出すと、1 つの副作用があります。subscribe()
を呼び出すときにウェブアプリに通知を表示する権限がない場合、ブラウザが権限をリクエストします。これは、UI がこのフローと連携する場合に便利ですが、より細かい制御が必要な場合(ほとんどのデベロッパーにはそうでしょう)は、前に使用した Notification.requestPermission()
API を使用してください。
PushSubscription とは何ですか?
subscribe()
を呼び出していくつかのオプションを渡すと、PushSubscription
に解決される Promise が返されます。コードは次のようになります。
function subscribeUserToPush() {
return navigator.serviceWorker
.register('/service-worker.js')
.then(function (registration) {
const subscribeOptions = {
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(
'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U',
),
};
return registration.pushManager.subscribe(subscribeOptions);
})
.then(function (pushSubscription) {
console.log(
'Received PushSubscription: ',
JSON.stringify(pushSubscription),
);
return pushSubscription;
});
}
PushSubscription
オブジェクトには、そのユーザーにプッシュ メッセージを送信するために必要な情報がすべて含まれています。JSON.stringify()
を使用して内容を出力すると、次のように表示されます。
{
"endpoint": "https://some.pushservice.com/something-unique",
"keys": {
"p256dh":
"BIPUL12DLfytvTajnryr2PRdAgXS3HGKiLqndGcJGabyhHheJYlNGCeXl1dn18gSJ1WAkAPIxr4gK0_dQds4yiI=",
"auth":"FPssNDTKnInHVndSTdbKFw=="
}
}
endpoint
は、push サービスの URL です。プッシュ メッセージをトリガーするには、この URL に POST リクエストを送信します。
keys
オブジェクトには、push メッセージで送信されるメッセージ データを暗号化するために使用される値が含まれます(このセクションの後半で説明します)。
有効期限切れを防ぐための定期的な再定期購入
プッシュ通知に登録すると、多くの場合、null
の PushSubscription.expirationTime
を受け取ります。理論上、これは定期購入が期限切れになることはないことを意味します(定期購入が期限切れになる正確な時点を示す DOMHighResTimeStamp
を受け取る場合とは対照的)。ただし、実際には、長期間プッシュ通知を受信しなかった場合や、プッシュ通知の権限を持つアプリをユーザーが使用していないことがブラウザで検出された場合は、定期購入が期限切れになることがあります。これを防ぐための 1 つのパターンは、次のスニペットに示すように、通知を受け取るたびにユーザーを再登録することです。これには、ブラウザが定期購入を自動的に期限切れにしないように、十分な頻度で通知を送信する必要があります。定期購入が期限切れにならないようにするためだけに、ユーザーに意図せずスパムを送信することのメリットとデメリットを慎重に検討する必要があります。結局のところ、長い間忘れられた通知の定期購入からユーザーを保護するために、ブラウザと戦おうとするのは避けるべきです。
/* In the Service Worker. */
self.addEventListener('push', function(event) {
console.log('Received a push message', event);
// Display notification or handle data
// Example: show a notification
const title = 'New Notification';
const body = 'You have new updates!';
const icon = '/images/icon.png';
const tag = 'simple-push-demo-notification-tag';
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag
})
);
// Attempt to resubscribe after receiving a notification
event.waitUntil(resubscribeToPush());
});
function resubscribeToPush() {
return self.registration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
return subscription.unsubscribe();
}
})
.then(function() {
return self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
});
})
.then(function(subscription) {
console.log('Resubscribed to push notifications:', subscription);
// Optionally, send new subscription details to your server
})
.catch(function(error) {
console.error('Failed to resubscribe:', error);
});
}
サーバーに定期購入を送信する
プッシュ サブスクリプションを取得したら、それをサーバーに送信する必要があります。方法は自由に決めることができますが、JSON.stringify()
を使用してサブスクリプション オブジェクトから必要なデータをすべて取得することをおすすめします。または、次のように手動で同じ結果を組み合わせることもできます。
const subscriptionObject = {
endpoint: pushSubscription.endpoint,
keys: {
p256dh: pushSubscription.getKeys('p256dh'),
auth: pushSubscription.getKeys('auth'),
},
};
// The above is the same output as:
const subscriptionObjectToo = JSON.stringify(pushSubscription);
定期購入の送信は、ウェブページで次のように行います。
function sendSubscriptionToBackEnd(subscription) {
return fetch('/api/save-subscription/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(subscription),
})
.then(function (response) {
if (!response.ok) {
throw new Error('Bad status code from server.');
}
return response.json();
})
.then(function (responseData) {
if (!(responseData.data && responseData.data.success)) {
throw new Error('Bad response from server.');
}
});
}
ノードサーバーはこのリクエストを受け取り、後で使用できるようにデータベースにデータを保存します。
app.post('/api/save-subscription/', function (req, res) {
if (!isValidSaveRequest(req, res)) {
return;
}
return saveSubscriptionToDatabase(req.body)
.then(function (subscriptionId) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify({data: {success: true}}));
})
.catch(function (err) {
res.status(500);
res.setHeader('Content-Type', 'application/json');
res.send(
JSON.stringify({
error: {
id: 'unable-to-save-subscription',
message:
'The subscription was received but we were unable to save it to our database.',
},
}),
);
});
});
サーバーに PushSubscription
の詳細が保存されているので、いつでもユーザーにメッセージを送信できます。
有効期限切れを防ぐための定期的な再定期購入
プッシュ通知に登録すると、多くの場合、null
の PushSubscription.expirationTime
を受け取ります。理論上、これはサブスクリプションが期限切れにならないことを意味します(サブスクリプションの有効期限が正確に示される DOMHighResTimeStamp
が届く場合とは対照的です)。しかし実際には、プッシュ通知を長時間受信していない場合や、ユーザーがプッシュ通知権限を持つアプリを使用していないことをブラウザが検出した場合などは、ブラウザで引き続き購読が期限切れになるのが一般的です。これを防ぐための 1 つのパターンは、次のスニペットに示すように、通知を受け取るたびにユーザーを再登録することです。これには、ブラウザが定期購入を自動的に期限切れにしないように、十分な頻度で通知を送信する必要があります。定期購入が期限切れにならないようにするためだけにユーザーにスパムを送信するのではなく、正当な通知の必要性とそのデメリットを慎重に検討してください。結局のところ、長い間忘れられた通知の定期購入からユーザーを保護するために、ブラウザと戦おうとするのは避けるべきです。
/* In the Service Worker. */
self.addEventListener('push', function(event) {
console.log('Received a push message', event);
// Display notification or handle data
// Example: show a notification
const title = 'New Notification';
const body = 'You have new updates!';
const icon = '/images/icon.png';
const tag = 'simple-push-demo-notification-tag';
event.waitUntil(
self.registration.showNotification(title, {
body: body,
icon: icon,
tag: tag
})
);
// Attempt to resubscribe after receiving a notification
event.waitUntil(resubscribeToPush());
});
function resubscribeToPush() {
return self.registration.pushManager.getSubscription()
.then(function(subscription) {
if (subscription) {
return subscription.unsubscribe();
}
})
.then(function() {
return self.registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('YOUR_PUBLIC_VAPID_KEY_HERE')
});
})
.then(function(subscription) {
console.log('Resubscribed to push notifications:', subscription);
// Optionally, send new subscription details to your server
})
.catch(function(error) {
console.error('Failed to resubscribe:', error);
});
}
よくある質問
この時点でよくある質問をいくつかご紹介します。
ブラウザで使用するプッシュ サービスを変更できますか?
いいえ。push サービスはブラウザによって選択されます。subscribe()
呼び出しで確認したように、ブラウザは push サービスにネットワーク リクエストを行い、PushSubscription を構成する詳細を取得します。
ブラウザごとに異なる Push サービスを使用する場合、API も異なるのでしょうか?
すべての push サービスで同じ API が使用されます。
この一般的な API は Web Push Protocol と呼ばれ、アプリケーションが push メッセージをトリガーするために行う必要があるネットワーク リクエストを記述します。
ユーザーがパソコンでチャンネル登録した場合、スマートフォンでもチャンネル登録されますか?
いいえ。ユーザーは、メッセージを受信するブラウザごとに push に登録する必要があります。また、この場合、ユーザーが各デバイスで権限を付与する必要があります。
次のステップ
- ウェブでのプッシュ通知の概要
- プッシュの仕組み
- ユーザーの登録
- 権限の UX
- ウェブプッシュ ライブラリを使用したメッセージの送信
- ウェブ プッシュ プロトコル
- プッシュ イベントの処理
- 通知の表示
- 通知の動作
- 一般的な通知パターン
- プッシュ通知に関するよくある質問
- 一般的な問題とバグの報告