まず、プッシュ メッセージを送信する権限をユーザーから取得し、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 の両方がサポートされていることがわかります。次のステップは、サービス ワーカーを「登録」することです。
サービス ワーカーを登録すると、サービス ワーカー ファイルの場所をブラウザに伝えます。このファイルは引き続き JavaScript ですが、ブラウザは push などのサービス ワーカー API への「アクセス権」を付与します。正確に言うと、ブラウザはファイルがサービス ワーカー環境で実行されるようにします。
サービス ワーカーを登録するには、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.js にあります。register() を呼び出した後、ブラウザはバックグラウンドで次の手順を実行します。
- サービス ワーカー ファイルをダウンロードします。 
- JavaScript を実行します。 
- すべてが正しく実行され、エラーがない場合、 - register()によって返された Promise は解決します。なんらかのエラーが発生すると、Promise は拒否されます。
register()が拒否された場合は、Chrome DevTools で JavaScript のスペルミスやエラーがないか再確認します。
register() が解決すると、ServiceWorkerRegistration が返されます。この登録を使用して、PushManager API にアクセスします。
PushManager API のブラウザの互換性
権限のリクエスト
サービス ワーカーを登録し、ユーザーを登録する準備が整いました。次のステップは、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 を拒否します。
ユーザーが [ブロック] ボタンをクリックした場合は、エグジット ケースとして処理する必要があります。この場合、ウェブアプリはユーザーに再度権限をリクエストできなくなります。ユーザーは、設定パネルに埋め込まれている権限の状態を変更して、アプリの「ブロックを解除」する必要があります。ユーザーに権限をリクエストする方法とタイミングを慎重に検討してください。ユーザーが [ブロック] をクリックした場合、その決定を簡単に取り消すことはできません。
幸い、権限を求める理由がわかっている限り、ほとんどのユーザーは権限を許可してくれます。
後ほど、いくつかの人気サイトがどのように権限をリクエストしているかを見てみましょう。
PushManager を使用してユーザーを登録する
サービス ワーカーが登録され、権限が付与されたら、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 オプション
プッシュがブラウザに初めて追加されたとき、デベロッパーがプッシュ メッセージを送信して通知を表示できないかどうかについて不明確な点がありました。ユーザーはバックグラウンドで何かが起こったことを認識しないため、これは通常サイレント プッシュと呼ばれます。
デベロッパーが、ユーザーに知らせることなく、ユーザーの位置情報を継続的にトラッキングするなどの不正行為を行う可能性があるという懸念がありました。
このシナリオを回避し、仕様作成者がこの機能を最適にサポートする方法を検討する時間を確保するため、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()プロミスによって返されます。
後でプッシュ メッセージを送信する場合は、アプリケーション サーバーの秘密鍵で署名された情報を含む 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 オブジェクトには、プッシュ メッセージで送信されるメッセージ データを暗号化するために使用される値が含まれます(このセクションの後半で説明します)。
有効期限切れを防ぐための定期的な再定期購入
プッシュ通知を登録すると、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);
    });
}
よくある質問
この時点でよくある質問をいくつかご紹介します。
ブラウザで使用するプッシュ サービスを変更できますか?
いいえ。プッシュ サービスはブラウザによって選択されます。subscribe() 呼び出しで説明したように、ブラウザはプッシュ サービスにネットワーク リクエストを行い、PushSubscription を構成する詳細を取得します。
ブラウザごとに異なる Push サービスを使用する場合、API も異なるのでしょうか?
すべてのプッシュ サービスは同じ API を想定しています。
この共通 API は ウェブ プッシュ プロトコルと呼ばれ、プッシュ メッセージをトリガーするためにアプリケーションが行う必要があるネットワーク リクエストを記述します。
ユーザーがパソコンでチャンネル登録した場合、スマートフォンでもチャンネル登録されますか?
いいえ。ユーザーは、メッセージを受信するブラウザごとにプッシュを登録する必要があります。また、ユーザーが各デバイスで権限を付与する必要があります。
次のステップ
- ウェブプッシュ通知の概要
- プッシュの仕組み
- ユーザーを登録する
- 権限の UX
- ウェブプッシュ ライブラリを使用したメッセージの送信
- ウェブ プッシュ プロトコル
- プッシュ イベントの処理
- 通知の表示
- 通知の動作
- 一般的な通知パターン
- プッシュ通知に関するよくある質問
- 一般的な問題とバグの報告