ChromeでW3C Push APIを使ってみた

  • 365
    いいね
  • 30
    コメント

※ (2015/11/27: 関連してFetch APIの使い方を簡単ながら別記事に書いてみました)

もう既に、ググると参考記事がいくらでも出てくるような内容ではありますが、個人的にいろいろと上手く行かなかったりしたこともあるので、参考までにまとめてみました。

さて、Chrome 42 (Android版を含む(!))では、Service Workersとの組み合わせでPush APIが使えるようになりました。これは、

  • Google Cloud Messaging (GCM)、Firebase Cloud Messaging (FCM)のプッシュ通知をWebアプリで受け取ることができる
  • Chrome for Androidでは、Chromeが起動していなくても通知を受信できる(!)
    • Firefox for Android (48+)でもFirefoxが起動していなくても通知を受信できるようになる予定(!)
  • Chromeは当初GCMベースで実装しているが、Chrome 50+とFirefox 46+はIETFのWeb Pushプロトコルに対応し、共通化が完了

というものでして、これは使うしかないでしょう!、というわけで、HTML5Rocksの記事を参考にして、自分でも試してみました。

なお、本記事の内容のままではプッシュ通知にメッセージ本体(ペイロード)を付けることができませんが、Chrome 50+, Firefox 46+(45ESRを除く), Firefox for Android 48+(予定)では、共通仕様のWeb Pushに対応し、暗号化ペイロード付きのプッシュ通知に対応しています

また、Chrome 52+ではFCM/GCM関連のキー等の設定を行わずにプッシュ通知が可能になります

まずはService Worker

注意事項として、Service Workerを使うには、WebアプリをHTTPSに対応したWebサーバ、もしくは、localhostに設置する必要があります。

実際にはnavigator.serviceWorker等のオブジェクトがあるかどうかをチェックしながら動作するようにスクリプトを書くことになりますが、簡単のため今回は省略。

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Push API sample</title>
    <link rel="manifest" href="manifest.json">
  </head>
  <body>
    <button id="register">インストール</button>
    <button id="push" disabled>プッシュ通知を有効にする</button>
  </body>
</html>
js/webapp.js
window.addEventListener('load', function() {
  document.getElementById('register').addEventListener('click', register, false);
  document.getElementById('push').addEventListener('click', setPush , false);
  navigator.serviceWorker.ready.then(checkPush);
}, false);

function register() {
  navigator.serviceWorker.register('push.js').then(checkNotification);
}

function checkNotification() {
  Notification.requestPermission(function(permission) {
    if(permission !== 'denied')
      document.getElementById('push').disabled = false;
    else
      alert('プッシュ通知を有効にできません。ブラウザの設定を確認して下さい。');
  });
}

このようにすると、「インストール」ボタンを押すとスクリプトpush.jsがService Workerとして常駐するようになります。

ここで、Service Workerスクリプト(ここではpush.js)はWebアプリのHTMLと同じかより上位のディレクトリに置いたほうがよいのでご注意下さい。例えば、スクリプトの場所js/push.jsのようにしてしまうと、親Webアプリのスクリプト上でnavigator.serviceWorker.readyがいつまで経ってもresolveできなくなってしまいます。

ではPush Manager

その前にFCM/GCM

ChromeのPush APIはFCM/GCMのプッシュ通知を利用するため、AndroidアプリでFCM/GCMを使う場合と同様にキー等を設定するか、Web Push標準のVAPIDを利用してサーバ認証の設定を行うか、いずれかの方法が必要になります(Firefoxでは少なくともFCM/GCM関連の設定は不要です)。

本記事ではFCM/GCMの設定を行うことでChromeのプッシュ通知に対応する手順を示します。なお、VAPIDを使う場合、FCM/GCMの利用登録とキー取得、マニフェストへのgcm_sender_idの記述、アプリケーションサーバからFCM/GCMサーバにPOSTする際のserver keyの付与が不要になります。

まずはFirebaseもしくはGoogle Developers Consoleで利用登録とキー取得を済ませましょう。

Firebaseの場合、プロジェクトを作成すると、もれなくFCMのServer KeyとSender IDが付与されます。プロジェクトを選択し、左上のプロジェクト名の右にあるアイコンをクリックしてSettingsを選択し、CLOUD MESSAGINGタブを開くと、必要なキーとIDが確認できます。

IMG_0136.jpg

FirebaseでGoogle Developer Consoleのプロジェクトをインポートした場合も、この手順で対応できます。

Google Developer Consoleの場合は、まず、プロジェクトで(無ければ作りましょう)、APIs & authの下のAPIsを選択し、Google Cloud Messaging for Androidを有効にしておきます。なお、最初の一覧には後者が見えてきませんが、"messaging"でキーワード検索するとちゃんと出てきます。

Screen Shot 2015-03-30 at 10.49.42 .png

続いて、APIs & authの下のCredentialsを選択し、Public API accessの下のCreate new keyをクリックしてserver keyを作成します。(一つのキーでAndroidとChrome (デスクトップを含む)の両方とも同じ仕組みでプッシュ通知できますので、server keyは一つで十分です。)

ここで作成したキーと、プロジェクトのOverviewで表示されるProject Numberを控えておきます。

Application Manifest

ChromeでWebアプリからプッシュ通知を登録するには、先ほど作成したProject NumberをWebアプリのマニフェストファイルに記入する必要があります(Firefoxでは不要です)。最低限、次のような内容を書けばプッシュ通知は使えるでしょう。

manifest.json
{
  "name": "(Webアプリの名前)",
  "short_name": "(Webアプリの短い名前(アプリアイコン用; パスのみでOK)",
  "icons": [{
    "src": "(アプリアイコン画像のURL)",
    "sizes": "256x256",
    "type": "image/png"
  }],
  "start_url": "(WebアプリのURL)",
  "display": "standalone",
  "gcm_sender_id": "(FCMのSender ID、もしくは、Google Developers ConsoleのProject Number)",
}

このマニフェストファイルの場所は、HTMLの<link>タグで指定します(上記index.html参照)。

なお、Chrome 43以前にも対応する場合は、次のような記述も追加する必要があります(Chrome 44で廃止されています)。

{
  "gcm_user_visible_only": true
}

やっとプッシュ通知の登録

上記js/webapp.jsの続きとして、次のような内容を追加します。

js/webapp.js
var subscription = null;

function checkPush(sw) {
  sw.pushManager.getSubscription().then(setSubscription, resetSubscription);
}

function setSubscription(s) {
  if(!s)
    resetSubscription();
  else {
    document.getElementById('register').disabled = true;
    subscription = s;
    var p = document.getElementById('push');
    p.textContent = 'プッシュ通知を解除する';
    p.disabled = false;
    registerNotification(s);
  }
}

function resetSubscription() {
  document.getElementById('register').disabled = true;
  subscription = null;
  var p = document.getElementById('push');
  p.textContent = 'プッシュ通知を有効にする';
  p.disabled = false;
}

function setPush() {
  if(!subscription) {
    if(Notification.permission == 'denied') {
      alert('プッシュ通知を有効にできません。ブラウザの設定を確認して下さい。');
      return;
    }

    navigator.serviceWorker.ready.then(subscribe);
  }

  else
    navigator.serviceWorker.ready.then(unsubscribe);
}

function subscribe(sw) {
  sw.pushManager.subscribe({
    userVisibleOnly: true
  }).then(setSubscription, resetSubscription);
}

function unsubscribe() {
  if(subscription) {
    // 自分のWebアプリサーバ等にプッシュ通知の解除を通知する処理をここに実装
    ...
    subscription.unsubscribe();
  }
  resetSubscription();
}

function registerNotification(s) {
  var endpoint = s.endpoint;
  // Chrome 43以前への対処
  if(('subscriptionId' in s) && !s.endpoint.match(s.subscriptionId))
    endpoint += '/' + s.subscriptionId;
  // 自分のWebアプリサーバ等にプッシュ通知を登録する処理をここに実装
  // endpointにプッシュサービスのエンドポイントのURLが格納される
  ...
}

まず、プッシュ通知は、navigator.serviceWorker.ready等からPromiseで取得できるServiceWorkerRegistrationオブジェクト(上記コードではsw)のプロパティpushManager経由で操作することとなります。

プッシュ通知に関するメソッドやプロパティは、主に次の4種類となります:

  • pushManager.subscribe(): プッシュ通知を登録し、その結果(subscription)をPromiseで受け取ります。
  • pushManager.getSubscription(): 登録されているプッシュ通知(subscription)をPromiseで取得します。
  • subscription.endpoint: 登録されているプッシュ通知サービスのエンドポイントのURL。GCMの場合は、GCMエンドポイントのURL( https://android.googleapis.com/gcm/send )の後にスラッシュ(/)とRegistration IDをつなげたものとなります。
  • subscription.unsubscribe(): 登録されているプッシュ通知を解除する。

将来的には上記のエンドポイントのURLを自分のアプリケーションサーバに通知することで、プッシュサービスの種類に依存せずにプッシュ通知が可能になる見通しですが、現状では、ここで取得できるURLをそのままGCMで使うとエラーになり、エンドポイントURL(最後の/は除く)とRegistration IDの部分を分けて扱う必要があります。

pushManager.subscribe()pushManager.getSubscription()によってプッシュサービスのエンドポイントURLを取得できますが、プッシュ通知を送信する側、例えばWebサーバ等にこのURLを通知し、Registration IDを取り出すと、そのサーバ等から、通知を登録しているWebアプリが動作する端末にプッシュ通知を送信することが可能となります。

ここで、Chrome 45以降では、pushManager.subscribe()で次のようなオプション指定が必須になっています(Firefoxでは不要です)。

pushManager.subscribe({ userVisibleOnly: true });

userVisibleOnlyオプションをtrueにすると、プッシュ通知時に必ずユーザに見える形で何らかの動作がブラウザやOS上で発生するようになります。なお、デフォルト値はfalseです。Chromeの場合、プッシュ通知受信時にService Workerのスクリプト上で明示的にshowNotification()が実行されなかった場合は、デフォルトの通知表示が行われるようになっているようです。

(なお、Chrome 44ではuserVisibleという名前になっていますので、ご注意下さい。)

いよいよプッシュ通知受信時の処理

プッシュ通知を受信した時の処理は、Service Workerのスクリプト(ここではpush.js)に実装します。

push.js
self.addEventListener('push', function(evt) {
  evt.waitUntil(
    self.registration.showNotification(
      '(プッシュ通知に表示するタイトル)',
      {
        icon: '(アイコンのURL(パスのみでOK))',
        body: '(プッシュ通知に表示する説明テキスト)',
        tag: '(識別用の適当なタグ("tag", "notification", 等)'
      }
    )
  );
}, false);

self.addEventListener('notificationclick', function(evt) {
  evt.notification.close();

  evt.waitUntil(
    clients.matchAll({ type: 'window' }).then(function(evt) {
      var p = location.pathname.split('/');
      p.pop();
      p = location.protocol + '//' + location.hostname + (location.port ? ':'+location.port : '') + p.join('/') + '/';
      for(var i = 0 ; i < evt.length ; i++) {
        var c = evt[i];
        if(((c.url == p) || (c.url == p + 'index.html')) && ('focus' in c))
          return c.focus();
      }
      if(clients.openWindow)
        return clients.openWindow('./');
    })
  );
}, false);

まず、プッシュ通知を受信した時に直ちに実行する処理は、Service Workerのpushイベントのリスナとして実装します。通常はプッシュ通知を表示する処理を行うわけですが、Web Notification APIを使うのではなく、self.registration.showNotification()から操作する必要がある点に注意が必要です。

さらに、プッシュ通知がクリック(タップ)された時の処理は、Service Workerのnotificationclickイベントのリスナとして実装します。上記のサンプルコードでは、開かれているウィンドウやタブのURLをチェックして、Service Workerの親Webアプリであればそのウィンドウやタブにフォーカスし、見つからなければ新たに親Webアプリのウィンドウを開く、という処理を行っています。

なお、残念ながら、ウィンドウやタブを操作する処理をpushのイベントリスナに実装しても、何も起こりません...

実際にプッシュ通知を試してみる

それでは、Webアプリ側からプッシュ通知の受信を登録してendpointを取得できたら、実際にプッシュ通知を(サーバから)Webアプリに送信してみましょう。

アプリケーションサーバ側からプッシュ通知を送信するには

FCM/GCMと同じ方法でプッシュ通知を送信

基本的には、HTTP POSTでGCMサーバにserver keyと、endpointから取り出したRegistration IDを送信すればプッシュ通知を送信できますが、curlを使える環境であれば、次のようにすればコマンドラインからプッシュ通知の送信が可能です。(参考: [改訂版]Google Cloud Messaging (GCM) でプッシュ配信する[Android])

registration_idsはJSONの配列のため、複数個指定することで複数のWebアプリに対してまとめてプッシュ通知を送信することも可能です。

curl --header "Authorization: key=(FCM/GCM server key)" \
  --header Content-Type:"application/json" \
  https://android.googleapis.com/gcm/send \
  -d "{\"registration_ids\":[\"(Webアプリで取得したエンドポイントURLから取り出したRegistration ID)\"]}"

この方法の場合、JSONのdataフィールドにFCM/GCMで送信するプッシュ通知の内容のデータを指定しても、ブラウザ(Service Worker)のPush APIには渡されない点に注意が必要です。ここで、プッシュ通知をService Workerで受けた時に、Service Workerでサーバからデータを取得するには、Fetch APIを使う必要があります。

プッシュ通知にメッセージ本体(ペイロード)を付与したい場合は、暗号化ペイロード付きのプッシュ通知を利用します。

プッシュ通知を送信すると

このようにしてプッシュ通知を送信すると、デスクトップ版Chromeの場合はChromeが起動している時に、Chrome for Androidの場合は端末が起動している時に(ホーム画面でもロック画面でも)、プッシュ通知を受信できるはずです。

デスクトップ版Chromeの場合

Screen Shot 2015-03-30 at 13.54.48 .png

Chrome for Androidの場合

Screenshot_2015-03-30-15-15-22.png

なお、ここで表示されるテキストの内容は、あくまでもService Workerのスクリプトでself.registration.showNotification()で指定した内容となります。

また、Androidではプッシュ通知受信時に、通知領域にアイコンが追加されるもののポップアップが表示されず、自分で通知領域から通知一覧を引き出さないと内容を確認できない、という挙動となるようです。ここで、Chrome 45以降では、showNotification()メソッドにオプションを指定することで端末を振動させることができますので、これを活用するとよいでしょう。

push.js
self.addEventListener('push', function(evt) {
  evt.waitUntil(
    self.registration.showNotification(
      '(プッシュ通知に表示するタイトル)',
      {
        icon: '(アイコンのURL(パスのみでOK))',
        body: '(プッシュ通知に表示する説明テキスト)',
        tag: '(識別用の適当なタグ("tag", "notification", 等)',
        vibrate: [400,100,400] // ミリ秒単位で振動と休止の時間を交互に任意の回数だけ配列に格納
      }
    )
  );
}, false);