ブラウザプッシュ通知が流行りだした
Webプッシュ、ブラウザプッシュなどと呼ばれる機能はブラウザのPush APIを使い実装され、現在ChromeとFirefoxで使えるようになっています。
毎週のように新たなブラウザプッシュが送れるサービスが登場していますが、基本的には一律ですべてのユーザーに送信するものが多いように感じます。
その中でもFaceBookはユーザー個別にネイティブアプリと同様の内容でブラウザにプッシュ通知を送っています。
ユーザー個別で通知内容をカスタマイズしたいのと、
その辺のサービスを使ってロックインされると困るので、FaceBookのコードを参考にしつつ自分で実装してみました。
ChromeとFirefoxで微妙にPushの送り方が違うので、今回はGCM(Google Cloud Messaging)用の解説です。
Pushを送ってみるとこんな感じ
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 キーも作っておいてください。
いつの間にかこれに一本化されてる
manifest.json
にgcm_sender_idを追加する
{
"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アプリに送信するときと同じです。
これ系はたくさんライブラリがあるのでこのへん使っておけば良いと思います。
- Java https://github.com/google/gcm
- Ruby https://github.com/sghael/speedy_gcm
- Ruby https://github.com/spacialdb/gcm
送信に必要なのはendpointではなくregistrationIdだという点に注意
ServiceWorkerのデバッグ
Developer Tools -> Resouces -> Service Workers でデバッグが可能です。
歯車に下向きの矢印のアイコンでファイルのアップデート、
ベルのアイコンでPush通知が再現されself.addEventListener("push", func)
が実行されます。
通常のChromeでServiceWorkerのデバッグをしているとちょこちょこ固まるので、Canaryでやった方がよいかもしれません。
chrome://serviceworker-internals/でブラウザ全体のServiceWorkerを把握できます
ServiceWorkerから別ファイル読み込み
ServiceWorkerで読み込むファイルはスコープの関係で、ドメイン直下にあることが好ましいですが、
サービスの構成や使っているフレームワークやgulpなどの制約でドメイン直下にファイルを置くことが難しい場合があります。
このようなときimportScripts
を使い回避することができます。
登録するServiceWorkerのファイルはStaticな形でドメイン直下に置き、実際に編集するファイルは別の場所に置いておくといった運用が可能です。
importScripts('/assets/js/sw.js')
だいたいRequireJSのようなイメージ
Push通知の表示実装
self.addEventListener("push", func)
を記述すると、Push通知が来た際に実行されます。
self.addEventListener("push", function(event) {
event.waitUntil(
self.registration.showNotification("Push通知タイトル", {
body: "Push通知本文"
})
)
})
ここまで実装できたらDeveloper Toolsでベルアイコンをクリックしてみてください。このような通知がくれば成功です。
殺風景なので少し設定を追加してみます。
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]
})
)
})
- iconに画像ファイルのパスを入れるとアイコンが表示されます。
- tagは同一の文字列が入った通知をまとめてくれ、連続で表示させないようにする設定です。
- actionsはまだCanaryでしか動作しませんがカスタムボタンを増やすことができます。押したaction名は
notificationclick
のevent.action
で取得できます。 - vibrateはスマートフォン限定で通知時に振動させられる。
ユーザー個別に通知内容のカスタマイズする
ブラウザのプッシュではネイティブアプリのように、プッシュにテキストやURLなどの情報を入れることはメッセージを暗号化しないとできません。
つまりブラウザのプッシュではプッシュが来たということしかわからないという仕様です。
なのでプッシュが来てからFetch API(XHRの新しくて使いやすいやつ)でサーバーに詳細内容を問い合わせて、
取得した結果を表示させるという処理が必要になります。
例えばこんなレスポンスを返すAPIがあったとします。
{
"title": "GAMY",
"icon": "/icon.png",
"body": "◯◯さんがあなたのコメントに返信しました。",
"url": "https://gamy.jp/notifications"
}
そしてJS側ではFetch API使った形に書き直します
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していくイメージです。
render :json => ["通知5", "通知4", "通知3", "通知2", "通知1"].pop
このレスポンスの情報をshowNotification
に入れ個別の内容を表示することができます。
どっちかっていうとサーバー側が頑張る感じ
通知をクリックしたときの処理
通知をクリックしたときには、その該当ページをブラウザで開く挙動を期待します。
showNotification
にdataを追加し、その中にurlを入れています。
クリックしたときに実行されるself.addEventListener('notificationclick', func)
を追加しました。
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で導入し始めました。
これでサーバーのアラートとか実装したい、メールより便利そう