【イラスト付き】プッシュ通知【利用方法】
はじめに
皆さんこんにちは。
今回はプッシュ通知で利用するAPIをご紹介します。
プッシュ通知を利用することでユーザーの興味を惹きつけることができます。
なお本記事ではプッシュ通知の運用方法については触れませんので、ご了承ください。
こんな人にオススメ
- プッシュ通知送信の流れが知りたい
- プッシュ通知の実装方法が知りたい
初めて学習する方にも分かるように、要点を絞って丁寧に解説していきます。
😋 プッシュ通知の全体像から実装方法をご紹介します♪
サンプルコードのリポジトリ
プッシュ通知とは
まずポイントをチェック
- アプリからユーザーに送信する通知
- サーバーから任意のタイミングで送信可能
- ブラウザを閉じた状態でも通知は表示される
プッシュ通知とは、アプリからユーザーに送信される通知です。新しい情報や更新情報など様々な内容を送信することができます。
プッシュ通知はブラウザが非表示の場合も表示されるため、タイムリーな情報を提供することができます。このようにプッシュ通知を利用することでユーザーの関心を惹く助けになります。
その一方で、通知がユーザーにとって煩わしかったり混乱を招くものにならないように、使い方には注意が必要です。
😋 任意のタイミングでユーザーに情報を送れます♪
プッシュ通知の構成要素
まずポイントをチェック
- プッシュAPI:プッシュ通知の管理
- 通知API:通知の表示
- ServiceWorker:通知受信時に動作
プッシュ通知の実現には3つの要素が関連します。プッシュAPIと通知APIとServiceWorkerです。
プッシュAPI(PushAPI)はプッシュ通知の根幹となる機能を持ちます。プッシュ通知に必要な設定や通知のクライアントへ送信などを行います。
通知API (NotificationAPI)は通知の表示に関する機能を提供します。通知の表示内容やクリック時の動作などを指定することができます。
ServiceWorkerはプッシュ通知受信時の動作を指定することができます。プッシュ通知の受信はServiceWorkerのイベントとして通知されます。通知APIの一部の機能はServiceWorkerにも拡張されているため、通知を表示することができます。
🍕 まとめると、プッシュAPIがプッシュ通知の手続きを行い、受信したプッシュ通知はServiceWorkerでイベント通知され、通知APIの機能で表示します。 |
---|
😋 プッシュ通知には複数の要素が関連しています♪
プッシュ通知の流れ
まずポイントをチェック
- サーバーからプッシュサービスへプッシュ通知を送信
- プッシュサービスからデバイスへプッシュ通知を送信
- ServiceWorkerが起動し、プッシュ通知を表示
プッシュ通知の送信から表示までには大きく3つのステップがあります。
🍕 まとめると、サーバーからプッシュ通知を送信しクライアントでプッシュ通知が表示されるまでの間を、プッシュサービスが取り持ちます。 |
---|
クライアント側ではプッシュサービスに加入します。この時購読オブジェクト(Subscriptionオブジェクト)を受け取ります。この購読オブジェクトには通知の送信先URL(エンドポイント)や鍵情報が含まれています。購読オブジェクトはサーバーに送付し、データベースやファイルなどに保管します。
サーバー側ではクライアントから受け取った購読オブジェクトを利用し、プッシュ通知を送信します。するとプッシュサービスへとプッシュ通知が渡ります。
プッシュサービスはサーバーから受け取ったプッシュ通知を、クライアントへ送信します。プッシュ通知を受信したクライアントではServiceWorkerにイベントが発生します。このイベントのイベントリスナーでプッシュ通知の表示を行います。
😋 プッシュ通知はプッシュサービスと購読オブジェクトが中継しています♪
プッシュ通知の実装ステップ
まずポイントをチェック
- プッシュサービスに加入し、購読オブジェクトを取得
- サーバーに購読オブジェクトを送付
- pushイベントで通知を表示
プッシュ通知の実装ステップをご紹介します。実装内容は「購読オブジェクトを取得しサーバーに渡す」「プッシュ通知に反応する」です。ステップ1からステップ3に分けて見ていきます。
ステップ1:プッシュサービスに加入し、購読オブジェクトを取得
プッシュサービスの加入と購読オブジェクトの取得は(ServiceWorkerではなく)メインのJavaScriptで行います。
まずは購読オブジェクトを取得します。通知の許可を行うとプッシュサービスに登録され、購読オブジェクトを取得できます。
購読オブジェクト取得時に公開鍵が必要になります。公開鍵と秘密鍵のペアはサーバーで作成し、サーバーから公開鍵を取得します。購読オブジェクト取得の際に指定する公開鍵は、Base64形式からUnit8Arrayに変換する必要があります。
ステップ2:サーバーに購読オブジェクトを送付
ステップ1で取得した購読オブジェクトをサーバーに送信します。特に意識することはなく通常通り、POSTリクエストでJSONとして送付します。サーバーでは受け取った購読オブジェクトをファイルやDBなどに保管します。
ステップ3:ServiceWorkerでpushイベントのリスナーを用意
最後にプッシュ通知を受け取った際の処理をServiceWorkerに用意します。プッシュ通知を受信するとServiceWorkerにイベントとして伝わります。このイベントリスナーでプッシュ通知を表示する処理を行います。
😋 実装には定型的なステップがあります♪
プッシュ通知の実装コード例
まずポイントをチェック
- プッシュサービスに加入し、購読オブジェクトを取得
- 既存の購読オブジェクトの取得:registration.pushManager.getSubscription()
- 新しく購読オブジェクトの取得:registration.pushManager.subscribe(オプション)
- 公開鍵の変換:urlBase64ToUint8Array関数(MDN で紹介されている関数を再利用)
- サーバーに購読オブジェクトを送付
- fetch関数でPOSTリクエスト
- ServiceWorkerでpushイベントのリスナーを用意
- プッシュ通知受信時のイベントリスナー:self.addEventListener(‘push’, 関数);
- プッシュ通知の表示:self.registration.showNotification(message)
- 【おまけ】プッシュ通知クリック時のイベントリスナー:self.addEventListener(‘notificationclick’, 関数);
前節のプッシュ通知の実装ステップに沿って、コード例をご紹介します。
ステップ1:プッシュサービスに加入し、購読オブジェクトを取得
前段として、まずはServiceWorkerの登録をします。navigator.serviceWorker.registerで登録することができます。引数はServiceWorkerのスクリプトファイルのパスです。このメソッドは非同期で動作するのでawaitキーワードを付けています。
// Service Workerの登録
const registration = await navigator.serviceWorker.register('./service_worker.js');
購読オブジェクトがすでに存在するかを確認します。購読オブジェクトを二重に登録してしまうと通知も二重に届いてしまいます。registration.pushManager.getSubscriptionを実行し、すでに購読オブジェクトがあるか確認します。このメソッドは非同期で動作するのでawaitキーワードを付けています。購読オブジェクトがない場合はnullに解決されます。
// subscriptionオブジェクトを取得
let subscription = await registration.pushManager.getSubscription();
if (!subscription) { // subscriptionオブジェクトがない場合
サーバー側で用意した公開鍵を取得し、Unit8Arrayに変換します。今回サーバー側では/keyへのGETリクエストで公開鍵を取得できるように実装しています。Unit8Arrayへの変換はurlBase64ToUint8Arrayメソッドで行っています。
// サーバー側で生成した公開鍵を取得し、urlBase64ToUint8Array()を使ってUit8Arrayに変換
const res = await fetch('/key');
const vapidPublicKey = await res.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
購読オブジェクトを取得します。registration.pushManager.subscribeでプッシュサービスに加入し購読オブジェクトを取得することができます。この際、ユーザーに通知の許可を尋ねるダイアログが表示されます。通知が許可されている場合は購読オブジェクトを取得します。このメソッドは非同期で動作するのでawaitキーワードを付けています。引数にはオプションオブジェクトを指定します。userVisibleOnlyプロパティにtrueを設定することで通知を表示することができます。applicationServerKeyプロパティにはUnit8Arrayに変換した公開鍵を指定します。
// ユーザがプッシュサービスに加入しSubscriptionオブジェクトを取得
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
これでステップ1は完成です。購読オブジェクトの取得ができました。
(async () => {
// Service Workerの登録
const registration = await navigator.serviceWorker.register('./service_worker.js');
// subscriptionオブジェクトを取得
let subscription = await registration.pushManager.getSubscription();
if (!subscription) { // subscriptionオブジェクトがない場合
// サーバー側で生成したパブリックキーを取得し、urlBase64ToUint8Array()を使ってUit8Arrayに変換
const res = await fetch('/key');
const vapidPublicKey = await res.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
// ユーザがプッシュサービスに加入しSubscriptionオブジェクトを取得
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
/* --------------ステップ2はここに追記されます-------------- */
}
})();
😋 プッシュサービスの加入から購読オブジェクトの取得まで実装できました♪
ステップ2:サーバーに購読オブジェクトを送付
購読オブジェクトをサーバーに送信するには、通常通りPOSTリクエストをすればOKです。今回サーバー側では/registerへのPOSTリクエストで購読オブジェクトを送信できるように実装しています。
// サーバーにSubscriptionオブジェクトを送信
await fetch('/register', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
これでステップ2は完成です。購読オブジェクトの送信ができました。
(async () => {
// Service Workerの登録
const registration = await navigator.serviceWorker.register('./service_worker.js');
// subscriptionオブジェクトを取得
let subscription = await registration.pushManager.getSubscription();
if (!subscription) { // subscriptionオブジェクトがない場合
// サーバー側で生成した公開鍵を取得し、urlBase64ToUint8Array()を使ってUit8Arrayに変換
const res = await fetch('/key');
const vapidPublicKey = await res.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
// ユーザがプッシュサービスに加入しSubscriptionオブジェクトを取得
subscription = await registration.pushManager.subscribe({
userVisibleOnly: false,
applicationServerKey: convertedVapidKey
});
// サーバーにSubscriptionオブジェクトを送信
await fetch('/register', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
}
})();
😋 購読オブジェクトの送信まで実装できました♪
ステップ3:ServiceWorkerでpushイベントのリスナーを用意
pushイベントはServiceWorkerで発生します。メッセージの内容はevent.data.textで取得できます。今回は単一の文字列をメッセージに指定しているためtextメソッドを利用しています。メッセージがJSONの場合はjsonメソッドで中身を取り出します。
通知の表示はself.registration.showNotificationで行います。引数には通知のタイトルを指定します。例には含まれていませんが、第二引数でオプションオブジェクトを指定することもできます。オプションオブジェクトでは通知で利用するアイコンなどの指定ができます。このメソッドは非同期で動作するためwaitUntilでラップし、通知が表示されるまでServiceWorkerが停止しないようにしています。
// pushイベント発生時にプッシュ通知を表示
self.addEventListener('push', event => {
const message = event.data.text();// 文字列を取得
// const { message } = event.data.json();// JSONを取得
event.waitUntil(
self.registration.showNotification(message)
);
});
こちらはおまけとして、プッシュ通知がクリックされた際の動作を指定してます。プッシュ通知がクリックされるとnotificationclickイベントが発生します。ここでクリック時の動作を指定することができます。今回はプッシュ通知をクリックした際にGoogleのトップページを新しいウィンドウで開くようにしています。clients.openWindowの引数のページを開くことができます。
// プッシュ通知クリック時にGoogleに遷移させる
self.addEventListener('notificationclick', () => {
clients.openWindow('https://www.google.com/')
});
これでステップ3は完成です。プッシュ通知を表示することができました。
// pushイベント発生時にプッシュ通知を表示
self.addEventListener('push', event => {
const message = event.data.text();// 文字列を取得
// const { message } = event.data.json();// JSONを取得
event.waitUntil(
self.registration.showNotification(message)
);
});
// プッシュ通知クリック時にGoogleに遷移させる
self.addEventListener('notificationclick', () => {
clients.openWindow('https://www.google.com/')
});
😋 ここまでがプッシュ通知を表示するまでのコードです♪
プッシュ通知の制限と開発時の注意
まずポイントをチェック
- HTTPSでのみ有効
- 自己発行証明書ではServiceWorkerは登録できない
- 開発時は強引にコマンドで起動して動作確認
プッシュ通知を利用する際は制限や注意があります。プッシュ通知はHTTPSでのみ動作します。そのため証明書が必要になります。開発時に自己発行証明書を利用する場合、注意が必要です。
ServiceWorkerは自己発行証明書の場合は登録することができません。そのため無理やりな方法でブラウザを起動させて動作確認をします。次のコマンドで自己発行証明書でも動作確認をすることができます。これは信頼できない証明書を無視して実行するためのコマンドなので開発時のみの利用にしましょう。
start chrome –ignore-certificate-errors –unsafely-treat-insecure-origin-as-secure=https://localhost:3000 –allow-insecure-localhost –user-data-dir=C:\chrome-set
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome –ignore-certificate-errors –unsafely-treat-insecure-origin-as-secure=https://localhost:3000 –user-data-dir=/Users/[ユーザ名]/Desktop/chrome-set
😋 自己発行証明書だとServiceWorkerは使えないので注意♪
【おまけ】通知APIのみのコード例
まずポイントをチェック
- 処理として通知を表示できる
- 通知の許可はプッシュAPIと統合されている
通知API(NotificationAPI)単体での通知の表示方法をご紹介します。
通知を表示するにはユーザーから許可を得る必要があります。Notification.requestPermissionで通知の許可を得ることができます。このメソッドは非同期で動作するためawaitをつけることができます。
通知の表示はNotificationインスタンスを生成することで行います。Notificationコンストラクタを利用して通知を表示します。引数には通知のタイトルとオプションオブジェクトを指定できます。オプションオブジェクトは必須ではありません。
通知に対する操作はNotificationインスタンスにイベントとして通知されます。通知を閉じた際の動作はcloseイベントで指定します。通知をクリックした際の動作はclickイベントで指定します。
// 通知の許可を確認
Notification.requestPermission();
// 通知を表示する関数
const btn = document.getElementById('btn');
btn.addEventListener('click', async () => {
const notification = new Notification('通知', { body: '通知の内容', icon: '../img/fish.png' });
notification.addEventListener('close', () => alert('通知を閉じた'));
notification.addEventListener('click', () => alert('通知をクリックした'));
});
😋 処理として通知を表示することができます♪
【おまけ】Node.jsとweb-pushでサーバー側を用意
まずポイントをチェック
- サーバー側のプッシュ通知対応はweb-pushを利用
- webPush.generateVAPIDKeys:Push通知用のVAPIDキーを生成
- webPush.setVapidDetails:プッシュメッセージの暗号化に使用するキー(Unit8Array)を設定
- webPush.sendNotification:プッシュ通知を送信
- ルートハンドラーをいくつか定義
- 公開鍵を返却するルートハンドラー
- 購読オブジェクトを受け取るルートハンドラー
- プッシュ通知を送信するルートハンドラー
おまけとしてサーバー側のコードの紹介をします。サーバー側はNode.jsでExpressとweb-pushを利用しています。
ステップ1:web-pushの準備
webPush.generateVAPIDKeysでPush通知用のVAPIDキーを生成します。
// Push通知用のVAPIDキーを生成
const vapidKeys = webPush.generateVAPIDKeys();
webPush.setVapidDetailsでステップ1で生成したキーを設定します。
// プッシュメッセージの暗号化に使用するキー(Unit8Array)を設定
webPush.setVapidDetails(
'https://localhost:3000/',
vapidKeys.publicKey,
vapidKeys.privateKey
);
ステップ2:プッシュ通知の送信
webPush.sendNotificationでプッシュ通知を送信します。第一引数にクライアントからもらった購読オブジェクトを指定します。第二引数は送信する通知の内容です。
// 保存されたサブスクリプションにプッシュ通知を送信
webPush.sendNotification(subscription, message);
完成形
const webPush = require('web-push'); // web-push導入
const app = express();
// Push通知用のVAPIDキーを生成
const vapidKeys = webPush.generateVAPIDKeys();
// プッシュメッセージの暗号化に使用するキー(Unit8Array)を設定
webPush.setVapidDetails(
'https://localhost:3000/',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// クライアントに公開鍵を返すルートハンドラ
app.get('/key', (req, res) => {
res.send(vapidKeys.publicKey);
});
// サブスクリプションを保存する配列
const subscriptions = [];
// サブスクリプションオブジェクトを受け取るルートハンドラ
app.post('/register', (req, res) => {
const subscription = req.body;
// サブスクリプションを配列に保存
subscriptions.push(subscription);
// レスポンスを返す
res.status(201).json({});
});
// プッシュメッセージを送信するルートハンドラ
app.post('/send-notification', async (req, res) => {
const message = 'Hello, world!';
// const message = '{"message" : "Hello, JSON"}';
// 保存されたサブスクリプションにプッシュ通知を送信
for (subscription of subscriptions) {
try {
await webPush.sendNotification(subscription, message);
} catch (e) {
console.log('このSubscriptionオブジェクトには送信できませんでした:' + JSON.stringify(subscription));
subscriptions.splice(subscriptions.indexOf(subscription), 1);
}
}
// レスポンスを返す
res.status(200).end();
});
😋 サーバーもクライアントの実装に対応するように定型的な記述があります♪
改めてソースコード全体を掲載
まずポイントをチェック
- /public/index.html:HTMLファイル
- /public/javascripts/index.js:メインのJSファイル
- /public/service_worker.js:ServiceWorkerのファイル
- /app.js:サーバーアプリのファイル
- /bin/www:実行の起点になるファイル
クライアント側
HTMLファイルには通知APIで通知を表示するためのボタンを用意しています。このボタンはプッシュ通知とは直接関係ありません。new Notificationの確認用のボタンです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Pushサンプル</title>
<script src="./javascripts/index.js" type="module"></script>
</head>
<body>
<h1>Pushサンプル</h1>
<button id="btn">通知</button>
</body>
</html>
メインのJavaScriptではプッシュ通知の準備とボタンのイベントリスナーの登録を行っています。
(async () => {
// Service Workerの登録
const registration = await navigator.serviceWorker.register('./service_worker.js');
// NotificaitonAPI 通知の許可を確認
const permission = await Notification.requestPermission();
if (permission === 'granted') {
// subscriptionオブジェクトを取得
let subscription = await registration.pushManager.getSubscription();
if (!subscription) { // subscriptionオブジェクトがない場合
// サーバー側で生成した公開鍵を取得し、urlBase64ToUint8Array()を使ってUit8Arrayに変換
const res = await fetch('/key');
const vapidPublicKey = await res.text();
const convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
// ユーザがプッシュサービスに加入しSubscriptionオブジェクトを取得
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: convertedVapidKey
});
// サーバーにSubscriptionオブジェクトを送信
await fetch('/register', {
method: 'POST',
body: JSON.stringify(subscription),
headers: {
'Content-Type': 'application/json'
}
});
}
}
})();
// VAPID公開鍵をUint8Arrayに変換する関数
// 参考:https://github.com/mdn/serviceworker-cookbook/blob/fb3b7c5584f89aaa0893d2d7eb9f7f6261dcfde4/tools.js#L4
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
// 通知を表示する関数
const btn = document.getElementById('btn');
btn.addEventListener('click', async () => {
const notification = new Notification('通知', { body: '通知の内容', icon: '../img/fish.png' });
notification.addEventListener('close', () => alert('通知を閉じた'));
notification.addEventListener('click', () => alert('通知をクリックした'));
});
ServiceWorkerではプッシュ通知の表示とクリック時の操作を定義しています。
// pushイベント発生時にプッシュ通知を表示
self.addEventListener('push', event => {
const message = event.data.text();// 文字列を取得
// const { message } = event.data.json();// JSONを取得
event.waitUntil(
self.registration.showNotification(message)
);
});
// プッシュ通知クリック時にGoogleに遷移させる
self.addEventListener('notificationclick', () => {
clients.openWindow('https://www.google.com/')
});
サーバー側
サーバー側の処理を定義しています。web-pushを利用してプッシュ通知の対応をしています。
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const webPush = require('web-push'); // web-push導入
const app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Push通知用のVAPIDキーを生成
const vapidKeys = webPush.generateVAPIDKeys();
// プッシュメッセージの暗号化に使用するキー(Unit8Array)を設定
webPush.setVapidDetails(
'https://localhost:3000/',
vapidKeys.publicKey,
vapidKeys.privateKey
);
// クライアントに公開鍵を返すルートハンドラ
app.get('/key', (req, res) => {
res.send(vapidKeys.publicKey);
});
// サブスクリプションを保存する配列
const subscriptions = [];
// サブスクリプションオブジェクトを受け取るルートハンドラ
app.post('/register', (req, res) => {
const subscription = req.body;
// サブスクリプションを配列に保存
subscriptions.push(subscription);
// レスポンスを返す
res.status(201).json({});
});
// プッシュメッセージを送信するルートハンドラ
app.post('/send-notification', async (req, res) => {
const message = 'Hello, world!';
// const message = '{"message" : "Hello, JSON"}';
// 保存されたサブスクリプションにプッシュ通知を送信
for (subscription of subscriptions) {
try {
await webPush.sendNotification(subscription, message);
} catch (e) {
console.log('このSubscriptionオブジェクトには送信できませんでした:' + JSON.stringify(subscription));
subscriptions.splice(subscriptions.indexOf(subscription), 1);
}
}
// レスポンスを返す
res.status(200).end();
});
module.exports = app;
アプリケーションの実行の起点になるファイルです。HTTPSの設定以外はexpress-generatorで作成した雛形をそのまま利用しています。
let https = require('https');
// HTTPS用の認証ファイル2つを設定
let options = {
key: fs.readFileSync('./certificate/key.pem'),
cert: fs.readFileSync('./certificate/cert.pem')
};
let server = https.createServer(options,app);
😋 以上がプッシュ通知の全体像です♪
おわりに
皆さん、お疲れ様でした。
ここまでご覧いただき、ありがとうございました。
プッシュ通知について確認をしていただきました。
プッシュ通知を行うには様々な手続きを経る必要がありますが、定型的な記述も多いので「そういうものか」と飲み込んで進めると良いと思います。
😋 これからもプログラミング学習頑張りましょう♪
参考リンク集(MDN Web Docs のリンク)
プッシュ API:https://developer.mozilla.org/ja/docs/Web/API/Push_API
通知 API:https://developer.mozilla.org/ja/docs/Web/API/Notifications_API
通知 API の使用:https://developer.mozilla.org/ja/docs/Web/API/Notifications_API/Using_the_Notifications_API
Web Push API Notifications best practices:https://developer.mozilla.org/en-US/docs/Web/API/Push_API/Best_Practices
通知とプッシュを利用して PWA を再エンゲージ可能にするには:https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps/Re-engageable_Notifications_Push
サンプルコード
push-sample-with-notification(ChanCode):https://github.com/ChanCode-Sample/JavaScript-API/tree/main/push-sample-with-notification
serviceworker-cookbook(MDN):https://github.com/mdn/serviceworker-cookbook
サンプルコード
serviceworker-cookbook(MDN):https://github.com/mdn/serviceworker-cookbook
Discussion