261
259

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ブラウザプッシュ通知とユーザー個別に内容を送信する実装方法 in GAMY

Last updated at Posted at 2016-02-17

ブラウザプッシュ通知が流行りだした

Webプッシュ、ブラウザプッシュなどと呼ばれる機能はブラウザのPush APIを使い実装され、現在ChromeとFirefoxで使えるようになっています。
毎週のように新たなブラウザプッシュが送れるサービスが登場していますが、基本的には一律ですべてのユーザーに送信するものが多いように感じます。
その中でもFaceBookはユーザー個別にネイティブアプリと同様の内容でブラウザにプッシュ通知を送っています。

ユーザー個別で通知内容をカスタマイズしたいのと、
その辺のサービスを使ってロックインされると困るので、FaceBookのコードを参考にしつつ自分で実装してみました。
ChromeとFirefoxで微妙にPushの送り方が違うので、今回はGCM(Google Cloud Messaging)用の解説です。

Pushを送ってみるとこんな感じ

GAMY

Service Worker

Push APIを使用するにはService Workerが動作している必要があります。

manifest.jsonの作りかたなど、Service Workerことはじめ的な内容はこちらで投稿しています。
http://qiita.com/narikei/items/4240f03542f29e313989

Google Developers Consoleでプロジェクトを登録

https://console.developers.google.com でプロジェクトを作成して、
この中にあるAPI ManagerからGoogle Cloud Messagingを有効にしておきます。
また、GCM送信用にサーバーのAPI キーも作っておいてください。
Google Developers Console
いつの間にかこれに一本化されてる

manifest.jsonにgcm_sender_idを追加する

/manifest.json
{
    "name": "GAMY(ゲーミー)- みんなのゲーム攻略メディア",
    "short_name": "GAMY攻略",
    "icons": [
        {
            "src": "/static/img/icon/144.png",
            "sizes": "144x144",
            "type": "image/png"
        }
    ],
    "start_url": "/",
    "display": "standalone",
    "theme_color": "#e91e63",
    "gcm_sender_id": "12345678910",
    "gcm_user_visible_only": true
}

gcm_sender_idにはGoogle Developers Consoleから取得するプロジェクト番号、
gcm_user_visible_onlyはChrome 44からDeprecateになっているのでなくても大丈夫です。

PushManagerのendopointの取得

ServiceWorkerのPushManagerを使いプッシュを送信するエンドポイントを取得できます。

window.addEventListener('load', function() {
  if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/serviceWorker.js')
      .then(function(registration) {
        return registration.pushManager.getSubscription().then(function(subscription) {
          if (subscription) {
            return subscription
          }
          return registration.pushManager.subscribe({
            userVisibleOnly: true
          })
        })
      }).then(function(subscription) {
        var endpoint = subscription.endpoint
        console.log("pushManager endpoint:", endpoint) // https://android.googleapis.com/gcm/send/******:******......
      }).catch(function(error) {
        console.warn("serviceWorker error:", error)
      })
  }
})

このコードを実行すると通知の表示に関するダイアログが表示され、これを許可すれば取得できます。
確認ダイアログ

取得できるendpointはhttps://android.googleapis.com/gcm/send/******......といった形です。
Chromeではhttps://android.googleapis.com/gcm/send/の後に続く文字列がGCMのregistrationIdとなります。

後でGCM送信時に使うためendopointはAjaxなどでサーバーに送信し、userIdと一緒にDBにでも保存してください。

サーバーからGCMの送信

GCMの送信方法は通常のAndroidアプリに送信するときと同じです。
これ系はたくさんライブラリがあるのでこのへん使っておけば良いと思います。

送信に必要なのはendpointではなくregistrationIdだという点に注意

ServiceWorkerのデバッグ

Developer Tools -> Resouces -> Service Workers でデバッグが可能です。
Service Workers Debugger
歯車に下向きの矢印のアイコンでファイルのアップデート、
ベルのアイコンでPush通知が再現されself.addEventListener("push", func)が実行されます。

通常のChromeでServiceWorkerのデバッグをしているとちょこちょこ固まるので、Canaryでやった方がよいかもしれません。

chrome://serviceworker-internals/でブラウザ全体のServiceWorkerを把握できます

ServiceWorkerから別ファイル読み込み

ServiceWorkerで読み込むファイルはスコープの関係で、ドメイン直下にあることが好ましいですが、
サービスの構成や使っているフレームワークやgulpなどの制約でドメイン直下にファイルを置くことが難しい場合があります。

このようなときimportScriptsを使い回避することができます。
登録するServiceWorkerのファイルはStaticな形でドメイン直下に置き、実際に編集するファイルは別の場所に置いておくといった運用が可能です。

/serviceWorker.js
importScripts('/assets/js/sw.js')

だいたいRequireJSのようなイメージ

Push通知の表示実装

self.addEventListener("push", func)を記述すると、Push通知が来た際に実行されます。

/assets/js/sw.js
self.addEventListener("push", function(event) {
  event.waitUntil(
    self.registration.showNotification("Push通知タイトル", {
      body: "Push通知本文"
    })
  )
})

ここまで実装できたらDeveloper Toolsでベルアイコンをクリックしてみてください。このような通知がくれば成功です。
通知1

殺風景なので少し設定を追加してみます。

/assets/js/sw.js
self.addEventListener("push", function(event) {
  event.waitUntil(
    self.registration.showNotification("Push通知タイトル", {
      icon: "/icon.png",
      body: "Push通知本文",
      tag: "push-test",
      actions: [{
        action: "act1",
        title: "ボタン1"
      }, {
        action: "act2",
        title: "ボタン2"
      }],
      vibrate: [200, 100, 200, 100, 200, 100, 200]
    })
  )
})

通知2

  • iconに画像ファイルのパスを入れるとアイコンが表示されます。
  • tagは同一の文字列が入った通知をまとめてくれ、連続で表示させないようにする設定です。
  • actionsはまだCanaryでしか動作しませんがカスタムボタンを増やすことができます。押したaction名はnotificationclickevent.actionで取得できます。
  • vibrateはスマートフォン限定で通知時に振動させられる。

ユーザー個別に通知内容のカスタマイズする

ブラウザのプッシュではネイティブアプリのように、プッシュにテキストやURLなどの情報を入れることはメッセージを暗号化しないとできません。
つまりブラウザのプッシュではプッシュが来たということしかわからないという仕様です。

なのでプッシュが来てからFetch API(XHRの新しくて使いやすいやつ)でサーバーに詳細内容を問い合わせて、
取得した結果を表示させるという処理が必要になります。

例えばこんなレスポンスを返すAPIがあったとします。

/notifications.json?endpoint=***********
{
  "title": "GAMY",
  "icon": "/icon.png",
  "body": "◯◯さんがあなたのコメントに返信しました。",
  "url": "https://gamy.jp/notifications"
}

そしてJS側ではFetch API使った形に書き直します

/assets/js/sw.js
function getEndpoint() {
  return self.registration.pushManager.getSubscription()
    .then(function(subscription) {
      if (subscription) {
        return subscription.endpoint
      }
      throw new Error('User not subscribed')
  })
}

self.addEventListener("push", function(event) {
  event.waitUntil(
    getEndpoint()
    .then(function(endpoint) {
      return fetch('/notifications.json?endpoint=' + endpoint)
    })
    .then(function(response) {
      if (response.status === 200) {
        return response.json()
      }
      throw new Error('notification api response error')
    })
    .then(function(response) {
      self.registration.showNotification(response.title, {
        icon: response.icon,
        body: response.body
      })
    })
  )
})

function getEndpoint()endpointを取得し、Fetchで投げるAPIにパラーメーターとして付け加えています。
このendpointでユーザーを判別し通知内容を返すようにサーバーに実装します。

デバイス毎に通知内容を格納する配列があり、古い順にPOPしていくイメージです。

notifications_controller.rb
render :json => ["通知5", "通知4", "通知3", "通知2", "通知1"].pop

このレスポンスの情報をshowNotificationに入れ個別の内容を表示することができます。

通知3

どっちかっていうとサーバー側が頑張る感じ

通知をクリックしたときの処理

通知をクリックしたときには、その該当ページをブラウザで開く挙動を期待します。

showNotificationにdataを追加し、その中にurlを入れています。
クリックしたときに実行されるself.addEventListener('notificationclick', func)を追加しました。

/assets/js/sw.js
self.addEventListener("push", function(event) {
  event.waitUntil(
    getEndpoint()
    .then(function(endpoint) {
      return fetch('/notifications.json?endpoint=' + endpoint)
    })
    .then(function(response) {
      if (response.status === 200) {
        return response.json()
      }
      throw new Error('notification api response error')
    })
    .then(function(response) {
      self.registration.showNotification(response.title, {
        icon: response.icon,
        body: response.body,
        data: {
          url: response.url
        }
      })
    })
  )
})

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

  var url = "/"
  if (event.notification.data.url) {
    url = event.notification.data.url
  }

  event.waitUntil(
    clients.matchAll({type: 'window'}).then(function() {
      if(clients.openWindow) {
        return clients.openWindow(url)
      }
    })
  )
})

これで通知をタップするとURLが開くようになります。

おわり

サービスワーカーの目玉とも言えるブラウザプッシュ機能ですが、
最小限の構成であれば数10行のコードで実現することができました。
すでにネイティブアプリへプッシュを行っているサービスであれば、けっこう簡単に実装できそうです。

実際にはもう少し考慮すべきところはありますが、自分が運用するGAMYで導入し始めました。
これでサーバーのアラートとか実装したい、メールより便利そう

261
259
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
261
259

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?