概要
Rails 4上において、一部ブラウザに対するプッシュ通知(Web Push)を実装する
VAPID(Voluntary Application Server Identification)を利用することで事前にプッシュサーバへの事前登録は必要ない
記事執筆時点(2017年04月28日)では
- Google Chrome
- Google Chrome for Android
- Firefox
- Opera
がWeb Pushに対応している
OperaはVAPIDに対応してない為、本記事ではChromeおよびFirefoxのみ扱うこととする
(本記事のコードをOpera(44.0 OS X)で動かした所、Operaインストール直後は正常にServiceWorker登録および購読登録でき、通知も送られたものの、一度Operaを終了し、再度起動した所、Registration failed - push service error
となり動かなくなった)
環境
- Macbook Pro 2015 OS X El Capitan (10.11.6)
- Ruby 2.2.1
- Rails 4.2
- gem serviceworker-rails 0.5.4
- gem webpush 0.3.1
- Google Chrome 57.0.2987 (OS X)
- Google Chrome for Android 57
- Firefox 52.0.2 (OS X)
https
もしくは localhost
でしか動かないので注意
VAPID鍵を使った認証を対応しているのは
GCMの登録が不要になったChromeのWeb Pushを試してみる
Chrome 52以降、Chrome for Android 52以降、Firefox 48以降
とのこと
OperaはWeb Push自体に対応したのが v25
以降なものの、記事執筆時点ではVAPIDには対応していない
Opera Developer 25 supports web notifications
実装
ServiceWorker準備
ServiceWorker
は自身があるディレクトリ以下の階層しか制御できないので注意
ルーティングすることができるので assets
以下に設置も可能(後述)
- manifest.json
{
"name": "WEB APP NAME",
"short_name": "WEB APP",
"start_url": "/"
}
最低限この3つ設定すれば作ればいけた
他に icons
でアイコン画像を設定できたりするがここでは触れない
- serviceworker.js(任意のファイル名でOK)
var CACHE_VERSION = 'v1';
var CACHE_NAME = CACHE_VERSION + ':sw-cache-';
function onSWInstall(event) {
return event.waitUntil(
caches.open(CACHE_NAME).then(function prefill(cache) {
return cache.addAll([]);
})
);
}
function onSWActivate(event) {
return event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.filter(function(cacheName) {
// Return true if you want to remove this cache,
// but remember that caches are shared across
// the whole origin
return cacheName.indexOf(CACHE_VERSION) !== 0;
}).map(function(cacheName) {
return caches.delete(cacheName);
})
);
})
);
}
function onSWPush(event) {
// event.data are null when Firefox's debugger push
// event.data are PushMessageData object and cannot exec event.data.json, event.data.text => 'Test push message from DevTools.'
var json = event.data ? event.data.json() : {"title" : "DEBUG TITLE", "body" : "DEBUG BODY"};
return event.waitUntil(
self.registration.showNotification(json.title, {
body: json.body,
icon: json.icon,
data: {
target_url: json.target_url
}
})
);
}
function onSWNotificationClick(event) {
event.notification.close();
return event.waitUntil(
clients.openWindow(event.notification.data != null ? event.notification.data.target_url : '/')
);
}
self.addEventListener('install', onSWInstall);
self.addEventListener('activate', onSWActivate);
self.addEventListener("push", onSWPush);
self.addEventListener("notificationclick", onSWNotificationClick);
今回はオフラインコンテンツを使用しないので return cache.addAll([]);
と空にしている
ServiceWorker
で通知を受け取った際、 data
で任意の値を送ることができる
今回はクリック時に指定のURLに遷移させたい為、
data: {
target_url: json.target_url
}
function onSWNotificationClick(event) { ... }
と指定している
body
, icon
, data
以外に指定できるオプションは
ServiceWorkerRegistration.showNotification()
等を参照すると良い
Gemインストール
今回は
# Gemfile
gem 'serviceworker-rails'
gem 'webpush'
を選択
webpush
はVAPIDの公開鍵、秘密鍵の生成、プッシュサーバ認証用ヘッダを付与して通知送信などをしてくれてとても便利
VAPID公開鍵、秘密鍵設定
コンソールで
require 'webpush'
vapid_key = Webpush.generate_key
puts vapid_key.public_key # 公開鍵
puts vapid_key.private_key # 秘密鍵
を実行し、公開鍵と秘密鍵を取得
環境変数に
WEB_PUSH_VAPID_PUBLIC_KEY="公開鍵"
WEB_PUSH_VAPID_PRIVATE_KEY="秘密鍵"
といった感じで設定し、 config/secrets.yml
に
web_push:
:vapid_public_key: <%= ENV['WEB_PUSH_VAPID_PUBLIC_KEY'] %>
:vapid_private_key: <%= ENV['WEB_PUSH_VAPID_PRIVATE_KEY'] %>
と設定しておくのが良さそう
ServiceWorkerのルーティング
前述の通り ServiceWorker
の scope
は自身のいるディレクトリ以下しか設定できないので、 asset pipeline
を利用して assets
に配置する場合、
config/initializers/serviceworker.rb
で
Rails.application.configure do
config.serviceworker.routes.draw do
match "/serviceworker-user.js" => "user/serviceworker/serviceworker.js"
end
end
と上記のように記述をすることで任意のパスに設定することができる
config/initializers/assets.rb
での precompile
設定も忘れずに
VIEW側にmanifest.json設定
<!-- 自身が設置した manifest.json のパスを記述する -->
<link rel="manifest" href="/manifest.json">
というように、各ページで manifest.json
を参照するように設定する
app/views/layouts
以下で設定するのが良いと思う
プッシュ通知購読・解除
app/assets/javascripts/任意のパス/serviceworker-companion.js.erb
あたりに
// WebPushが使えるか
var isEnableWebPushBrowser = function() {
// SWが使えないブラウザ
if (!navigator.serviceWorker) {
return false;
}
var ua = navigator.userAgent.toLowerCase();
// UAにChromeが含まれている為、明示的にEdgeとOpera(Webkit)を弾く
if (ua.indexOf("edge") >= 0 || ua.indexOf("opr") >= 0) {
return false;
}
// Chrome 52 以降OK
if (ua.match(/chrom(e|ium)/)) {
var raw = ua.match(/chrom(e|ium)\/([0-9]+)\./);
if (raw && parseInt(raw[2], 10) >= 52) {
return true;
}
}
// Firefox 48 以降OK
if (ua.indexOf("firefox") >= 0) {
var raw = ua.match(/firefox\/([0-9]+)\./);
if (raw && parseInt(raw[1], 10) >= 48) {
return true;
}
}
return false;
};
// WebPushが使えるブラウザの場合
// NOTE: 今回はWebPushだけなので。他でSW使いたかったら要変更
if (isEnableWebPushBrowser()) {
var convertWebPushSubscriptionJson = function(subscription) {
var jsonData = {};
if (subscription) {
jsonData = {
endpoint: subscription.endpoint,
key: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('p256dh')))).replace(/\+/g, '-').replace(/\//g, '_'),
auth: btoa(String.fromCharCode.apply(null, new Uint8Array(subscription.getKey('auth')))).replace(/\+/g, '-').replace(/\//g, '_')
};
}
return jsonData;
};
// ブラウザPush通知を有効にする
var webPushSubscribe = function() {
return navigator.serviceWorker.getRegistration().then(function(registration) {
// ブラウザPush通知用VAPID
var vapidPublicKey = new Uint8Array(<%= Base64.urlsafe_decode64(Rails.application.secrets.web_push[:vapid_public_key]).bytes %>);
return registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidPublicKey
}).then(function(subscription) {
// API叩いてDBに登録するなどの処理
console.log(convertWebPushSubscriptionJson(subscription));
return;
})["catch"](function(err) {
return console.log(err);
});
});
};
// ブラウザPush通知を無効にする
var webPushUnSubscribe = function() {
return navigator.serviceWorker.getRegistration().then(function(registration) {
return registration.pushManager.getSubscription().then(function(subscription) {
if (!subscription) {
return;
}
return subscription.unsubscribe().then(function(success) {
// API叩いて無効にするなどの処理
return;
})["catch"](function(error) {
return console.log(error);
});
});
});
};
// SWインストール
// NOTE: SWインストール済みかどうかはブラウザ側で判断される
navigator.serviceWorker.register('/serviceworker-user.js', { scope: '/' });
}
と書いて、あとは任意のタイミングでそれぞれ叩くと良い
(ブラウザ判定とか結構適当…)
ちなみにブラウザ側で通知をブロックすることができるのだが、
// javascript
if (Notification.permission == "denied") {
console.log('ブロックされています');
}
とブロックされているか判定することができる
プッシュ通知送信
rubyで
require 'webpush'
payload = {
endpoint: @endpoint, # ブラウザでregistration.pushManager.getSubscription()で取得したsubscriptionのendpoint
p256dh: @p256dh, # 同じくsubscriptionのp256dh
auth: @auth, # 同じくsubscriptionのauth
ttl: @ttl, # 任意の値
vapid: {
subject: 'mailto:info@example.org', # APPサーバのコンタクト用URIとか('mailto:' もしくは 'https://')
public_key: Rails.application.secrets.web_push[:vapid_public_key],
private_key: Rails.application.secrets.web_push[:vapid_private_key]
},
message: {
icon: 'https://example.com/images/demos/icon-512x512.png',
title: title,
body: body,
target_url: target_url # 任意のキー、値
}.to_json
}
# NOTE: payload_sendの例外
# RuntimeError の場合 Webpush::Error
# Error の場合 Webpush::ConfigurationError
# Net::HTTPGone 410 の場合 Webpush::InvalidSubscription
# Net::HTTPSuccess 2XX 以外の場合 Webpush::ResponseError
Webpush.payload_send(payload)
で送れる
受取側(ServiceWorker)で
function onSWPush(event) {
var json = event.data.json(); // payloadのmessage
と受け取るので、とりあえず使いたいデータがあれば :message
に混ぜると良い
ちなみに icon
で指定するアイコンサイズは、Google的には以前は 192x192 px
を推奨していて(Add recommendations for icon size on Web Push)
記事執筆時点では 192px以上
を推奨し、例文では 512x512 px
を使っている(Displaying a Notification)
上記ドキュメントや WebFundamentals/src/content/en/fundamentals/engage-and-retain/push-notifications/ 以下のページでは他のオプションや挙動など解説されているので興味があればぜひ
デバッグ
ブラウザのプッシュ通知のデバッグ
を参考に
あと最近ではブラウザ付属のデベロッパーツールでもデバッグできるようになっている
ちなみにChromeの デベロッパーツール>Application>ServiceWorker
には登録された ServiceWorker
の動作確認用の機能があるのだが、Push
を押すと
function onSWPush(event) { ... }
の event.data
が PushMessageData
オブジェクトできて、中身がテキストデータで event.data.json
でパースできずエラーになるのが辛い
Firefoxでは about:debugging#workers
の プッシュ
ボタンを押すと event.data
が undefined
でくるので三項演算子で制御できるのが助かる
その他
上記の記述では ServiceWorker
インストール直後は active
にはならなかったので注意
ゆくゆくはインストール後すぐに active
にしたい
Firefoxでデバッグ中、アップデートで再起動が必要となった際に、何故か
navigator.serviceWorker
が未定義になって困った
再起動したら直った
追記(2018/10/5)
#<Net::HTTPBadRequest 400 UnauthorizedRegistration readbody=true>
というエラーが出る場合があるとのことで、そのようなエラーが出た場合は下記記事を読むと解決するようだ
Railsでwebpush(vapid)しようと思ったら、"Unauthorized"エラーが出た話
@erb_owl++
このエラーはvapid
のオプションで
expiration: 12 * 60 * 60 # defaultは 24 * 60 * 60
と12時間程度にすると直るようだ
このオプションは、下記の通りgem内のJWTトークン発行にて使われている
# lib/webpush/request.rb
def jwt_payload
{
aud: audience,
exp: Time.now.to_i + expiration,
sub: subject,
}
end
def expiration
@vapid_options.fetch(:expiration, 24*60*60)
end
Googleが公開しているWeb Fundamentals内の Web Push: Common Issues and Reporting Bugs によると
You'll receive an UnauthorizedRegistration error in any of the following situations:
The expiration is invalid in your JWT, i.e. the expiration exceeds 24 hours or the JWT has expired.
と記載があるので24時間以内を設定すればOKなのだが、時計がずれてるとか何らかの理由で24時間以上後の時刻が設定されてしまっているのだろう
エラーが出るのなら24時間以内に収まるように設定する必要があるので注意
参照
Service Worker
Web Push
Opera Developer 25 supports web notifications
Service Workerを利用したWeb Push通知環境の構築【導入編】
GCMの登録が不要になったChromeのWeb Pushを試してみる
Sending Web Push Notifications from Rails
Using Web Push Notifcations with VAPID
ServiceWorkerRegistration.showNotification()
Displaying a Notification
Gem WebPush
Github WebFundamentals/src/content/en/fundamentals/engage-and-retain/push-notifications/
Web Notifications W3C Recommendation 22 October 2015
ブラウザのプッシュ通知のデバッグ