ChromeでW3C Push APIを使ってみた

※ (2017/7/4: *Chrome以外のブラウザとの相互運用性を考慮する場合は、Web Pushへの移行を推奨します。Web Pushについては[[改訂版] Web Pushでブラウザにプッシュ通知を送ってみる](http://qiita.com/tomoyukilabs/items/2ae4a0f708a1af75f13e)[[旧仕様対応版](http://qiita.com/tomoyukilabs/items/217915676603fda73b0a)]にまとめています。*(なお、AndroidやiOSへのFirebaseプッシュ通知と統合して運用する場合は、[Firebase JavaScript SDKの利用](https://firebase.google.com/docs/cloud-messaging/js/client)を検討した方がよいかもしれません。))

※ (2015/11/27: *[関連してFetch APIの使い方を簡単ながら別記事に書いてみました](http://qiita.com/tomoyukilabs/items/9b464c53450acc0b9574)。*)

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

さて、Chrome 42 (Android版を含む(!))では、[Service Workers](http://www.w3.org/TR/service-workers/)との組み合わせで[Push API](http://w3c.github.io/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の記事](http://updates.html5rocks.com/2015/03/push-notificatons-on-the-open-web)を参考にして、自分でも試してみました。

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

また、Chrome 52+では[FCM/GCM関連のキー等の設定を行わずにプッシュ通知が可能になります](http://qiita.com/tomoyukilabs/items/9346eb44b5a48b294762)。

# まずはService Worker

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

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

```html: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: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を利用してサーバ認証の設定を行う](http://qiita.com/tomoyukilabs/items/9346eb44b5a48b294762)か、いずれかの方法が必要になります(Firefoxでは少なくともFCM/GCM関連の設定は不要です)。

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

まずは[Firebase](https://console.firebase.google.com)もしくは[Google Developers Console](https://console.developers.google.com/)で利用登録とキー取得を済ませましょう。

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

![IMG_0136.jpg](https://qiita-image-store.s3.amazonaws.com/0/48556/498ebb2b-77e8-551d-982b-7c9bfb01c24b.jpeg)

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](https://qiita-image-store.s3.amazonaws.com/0/48556/c482b506-b8ab-89fc-8745-2cde7455905e.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では不要です)。最低限、次のような内容を書けばプッシュ通知は使えるでしょう。

```json: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で廃止されています](https://www.chromestatus.com/feature/5778950739460096))。

```json
{
  "gcm_user_visible_only": true
}
```

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

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

```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では不要です)。

```js
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]](http://dev.classmethod.jp/smartphone/android/gcm/))

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

```bash
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を使う](http://qiita.com/tomoyukilabs/items/9b464c53450acc0b9574)必要があります。

プッシュ通知にメッセージ本体(ペイロード)を付与したい場合は、[暗号化ペイロード付きのプッシュ通知](http://qiita.com/tomoyukilabs/items/217915676603fda73b0a)を利用します。

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

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

## デスクトップ版Chromeの場合
![Screen Shot 2015-03-30 at 13.54.48 .png](https://qiita-image-store.s3.amazonaws.com/0/48556/2d82a34f-62fe-7377-abb5-82040d15c62c.png)

## Chrome for Androidの場合
![Screenshot_2015-03-30-15-15-22.png](https://qiita-image-store.s3.amazonaws.com/0/48556/f28c53ef-27a3-56e4-4854-afa351aaf2bd.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);
```