DRYな備忘録

Don't Repeat Yourself.

webブラウザにPush通知送るサーバとjsのサンプル

このドキュメントは

以下の2つのドキュメントをよりプリミティブに理解するためのDRYな備忘録です。

  1. Adding Push Notifications to a Web App  |  Web  |  Google Developers
  2. The Web Push Protocol  |  Web  |  Google Developers

f:id:otiai10:20170619113329p:plain

背景

かつて サーバからブラウザにプッシュ通知を送りたい(非WebSocket、非ロングポーリング) - DRYな備忘録 これ書いたけど、改めてProgressive Web Appのドキュメント行ったらアホみたいに分かりづらくなってて不条理を感じたので書きます。

サーバからPush通知がブラウザに対して送れて、ブラウザのjsが閉じててもServiceWorkerが生きてるからNotificationが出る、っていうやつです。最近だとウェブのGmailとかFacebook Messanger若干うざいやつとかでよく見る気がします。

この公式ドキュメント、パッとサンプル落としてきてハイ動くでしょ!っていうタイプなのか最小限の要素を理解させつつ動かすタイプなのか、完全にどっち付かずのドキュメントになっていて、若干わかりにくいです。

今回は、WebPushProtocolにおける暗号化されたbodyを送る、というところを除き、クライアントのコードとサーバのコードを書いたので、そのログを備忘録として晒します。

概念の確認

サービスとしては、3つです

  1. ブラウザ
    • ネイティブアプリ開発における「デバイス」だと理解して問題ないです
    • ブラウザではふたつのことをします
      • ServiceWorkerを登録し、登録したServiceWorkerにプッシュをsubscribeさせ、PushSubscriptionモデルを取得します
        • これが、ApplePushNotificationにおけるDevice Token的なものです
      • ServiceWorker内部では、タブのJavaScriptが死んでもbackgroundで動き続けるイベントリスナーを登録します
        • pushが来たとき、というイベントリスナーもここで定義します
  2. 自前サーバ
    • ブラウザで登録・取得されたPushSubscriptionモデルのデータを、自作アプリケーションにおけるユーザなどと紐付けて保存しておく必要があります
    • 何らかのトリガーで(たとえばユーザによる他ユーザへの呼びかけなど)下記の「Pushサービス」に「このsubscription endpointへこの内容でプッシュ通知送ってちょんまげ」というリクエストをする役割があります
  3. Pushサービス
    • 各デバイスに対してPushを実際に送るサービスです
    • Web-Push-Protocolのインターフェースを満たすサービスである必要があります
      • Chromeではfcm.googleapis.com
      • Firefoxではupdates.push.services.mozilla.com
      • が、それぞれPushサービスとして採用されます
    • 僕らは自前サーバからこのエンドポイント叩くだけなのでその違いを意識する必要は無い ← ここだいじ!

うるせえ動くコード見せろ

ウッス

github.com

こんな感じに動きます

f:id:otiai10:20170619200505p:plain

実際の処理

  1. ブラウザに登録されるServiceWorker
    • ソースコード: service-worker.js
    • やること
      • 自分自身に何かイベントがあったらそれをハンドリングする
        • install, activate, push
    • 必要なもの
      • 無い。ぶっちゃけ登録されるServiceWorkerがやるべき責務は小さい。
  2. ウェブページのmain
    • ソースコード: main.js
    • やること
      • ServiceWorkerをブラウザにregisterする
      • そしたらregistration.pushManager.subscribe()というのを呼ぶ
      • 上記で得られたsubscriptionモデルを自作サーバへ送りつける
        • このsubscriptionというのが、Pushを提供するGCMサーバのエンドポイントと、このデバイス(ブラウザ)のDeviceToken的なものを持っている
    • 必要なもの
      • ApplicationServerPublicKeyが必要
        • サーバのみが知りうるApplicationServerPrivateKeyに対をなすもの
  3. Pushをトリガーする自前サーバ
    • ソースコード: server.js(もちろんjsで書く必要は無い)
    • やること
      • ウェブページのjsから送られてきたsubscriptionの保存
      • 任意のトリガーでそのsubscriptionへPushを送る
        • 厳密にはPushを"送る"のはgcmなりMozilaPushServiceなので、「Pushサービスが提供したエンドポイントを叩く」のほうが正確
        • また、Pushサービスを叩くときのリクエストはWeb-Push-Protocolで定められた方法で暗号化されている必要があるが、今回はこれはweb-pushライブラリにすべて任せている
    • 必要なもの
      • ApplicationServerPrivateKeyが必要

注意点・ハマったところ

ServiceWorkerの更新ができない

  • 問題: ページをリロードしても編集したServiceWorkerが適用されていない
  • 原因: ServiceWorkerのライフサイクルは「ブラウザ自体」と「ウェブページのドメイン」によって定められるのであって、「ウェブページのタブ(いわゆるいつものブラウザのjsランタイム)」ではないので、ページをリロードしても更新されない
  • 解決: getRegistraionからのunregisterを呼ぶ。あるいはchrome://serviceworker-internals/に行き該当ウェブページに登録されているServiceWorkerを[Unregister]ボタンで削除するなりする必要がある

Subscriptionの更新ができない

InvalidStateError: Registration failed - A subscription with a different applicationServerKey (or gcm_sender_id) already exists; to change the applicationServerKey, unsubscribe then resubscribe.
  • 問題: registration.pushManager.subscribe()を呼ぶと上記のようなエラーが出ることがある。
  • 原因: エラーメッセージでは「別のapplicationServerKeyを使ったsubscriptionがすでにあるので、applicationServerKeyを変えてunsubscribeしてからもっかいsubscribeしてね」と言っている。基本的にはApplicationServerPublicKeyApplicationServerPrivateKeyの組み合わせは、1つのプロジェクトについてただ1つだけなので、このようなエラーは起きないと思うが、開発中とかだと起きるかもしれませんねハイ。上記のサンプルでは、npmのpost-installで1度だけVAPIDを作るようにしてます
  • 解決: エラーメッセージにおとなしく従う

ApplicationServerKeyとは

InvalidAccessError: Failed to execute 'subscribe' on 'PushManager': The provided applicationServerKey is not valid.
Failed to execute 'atob' on 'Window': The string to be decoded is not correctly encoded.
  • 問題: subscribeするときに、上記のようなエラーが出ることがある。どうやらapplicationServerKeyがよくないようだが、そもそもApplicationServerKeyって何なのよ。
  • 原因: 今回いちばんハマったところなんですが、ApplicationServerKeyっていうのは、WebPushProtocolの文脈でのただの言い方で、その他いろいろな文脈とは関係ない、ただの、定められた方法で生成されたPublicKeyとPrivateKeyのペアというだけです。ここにGCM_API_KEYとかFCM_SERVER_KEYとか突っ込んで困る、というケースが多そう。まあ僕もなんですけど。RSAとかも無関係、名前似てるけど。
  • 解決: ほんで、この「Keyのペア」は、今回はweb-pushライブラリのgenerateVAPIDKeysに全部まかせています。PushサーバとしてGCMやMozilaPushServiceを叩くことこそあれど、今回のサンプルはGCM, FCM, MPSを関知しない、Web-Push-Protocolを用いたアプリケーションの構成のサンプルという位置づけです。

雑感

DRYな備忘録として

Building Progressive Web Apps: Bringing the Power of Native to the Browser

Building Progressive Web Apps: Bringing the Power of Native to the Browser

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-

はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-