Next(app router)とRailsでweb pushを実現する
web pushに対応した時の内容を備忘録代わりに残しておく
Rails側
gemのインストール
web-pushというgemを使います、ハイフンなしのwebpushというgemはメンテされてないので注意
VAPID公開鍵と秘密鍵の生成
WebPush.generate_key
で公開鍵と秘密鍵を生成してメモしておきます
# One-time, on the server
vapid_key = WebPush.generate_key
# Save these in your application server settings
vapid_key.public_key
vapid_key.private_key
通知処理の実装
以下のようなmodelを作っておき、subscribeされたときにエンドポイント等をuserに紐づけて保存しておきます。
あとは通知を送りたいタイミングでweb_pushを呼ぶことで通知を送れます。
messageの中は { title:, body:, target_url: }
な構造のhashを渡します。
# frozen_string_literal: true
# == Schema Information
#
# Table name: web_push_subscriptions
#
# id :bigint not null, primary key
# user_id(user id) :bigint not null
# endpoint(web push endpoint) :string(255) not null
# p256dh(web push p256dh) :string(255) not null
# auth(web push auth) :string(255) not null
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_user_id_on_web_push_subscriptions (user_id)
#
class WebPushSubscription < ApplicationRecord
def web_push(message)
WebPush.payload_send(**payload(message))
rescue WebPush::ExpiredSubscription,
WebPush::InvalidSubscription => e
Rails.logger.warn "WebPush Warn: #{e.message} #{self}"
# expiredやinvalidな時はdestroyして購読解除する
destroy!
rescue WebPush::ResponseError => e
Rails.logger.error "WebPush Error: #{e.message} #{self}"
end
private
def payload(message)
{
message: message.to_json,
endpoint:,
p256dh: p256dh_key,
auth: auth_key,
vapid: {
private_key: # 生成したVAPID秘密鍵をENVとかから読み取る,
public_key: # 生成したVAPID公開鍵をENVとかから読み取る
}
}
end
end
Next側
manifest.jsonの作成
{
"theme_color": "#12486B",
"background_color": "#ffffff",
"display": "standalone",
"scope": "/",
"start_url": "/",
"name": "hoge app",
"short_name": "hoge"
}
各キーの定義は以下を参照
上記manifestではicon記載してないですが、icon付ける場合必要なiconサイズに関しては以下を参照
一括でicon生成してくれるサイトもあるみたいです
service workerの登録とsubscribe
public/service-worker.js
を設置
通知を表示するのと通知をクリックした際に対象のURLを開いているタブがあればfocusし、なければ新しく開いてます
self.addEventListener('push', (event) => {
// Get the push message
var message = event.data.json();
// Display a notification
event.waitUntil(
self.registration.showNotification(message.title, {
body: message.body,
icon: './favicon.ico',
data: { target_url: message.target_url }
})
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients
.matchAll({
type: 'window'
})
.then((clientList) => {
for (const client of clientList) {
if (
client.url === event.notification.data.target_url &&
'focus' in client
) {
return client.focus();
}
}
if (clients.openWindow) {
return clients.openWindow(event.notification.data.target_url);
}
})
);
});
以下をhookとかにして app/layout.tsx
から読み込むようにしておきます
私はAppLayoutというcomponentを作って、そいつをlayout.tsxから読み込んでいます
NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEYに生成したVAPID公開鍵を入れておき、Uint8Arrayに変換します
Next.js 13.4以降はreactStrictModeがデフォルトでtrueになっており、useEffectが2回呼ばれるので注意です。
回避方法はこの辺りが参考になります
const base64ToUint8Array = (base64: string | undefined): Uint8Array => {
if (base64 === undefined) return new Uint8Array(0);
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
const b64 = (base64 + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(b64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; i += 1) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
};
export const useWebPushSubscribe = (): void => {
useEffect((): void => {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
navigator.serviceWorker.ready.then((reg) => {
reg.pushManager.getSubscription().then((subscription) => {
if (subscription !== null) return;
registration.pushManager
?.subscribe(subscribeOptions)
.then((pushSubscription) => {
// returnしてるだけですがここにserverにpushSubscriptionを登録するリクエスト処理を実装します
return {
endpoint: sub.endpoint,
p256dh: sub.keys.p256dh,
auth: sub.keys.auth
};
});
});
});
}
});
};
その他
Nextでweb pushで検索すると next-pwa
を使う記事が多くHITする
これはメンテされておらず、有志がforkしてapp routerに対応したものがある
これを使うことで serviceWorker
の登録を記載する必要がなかったり、Service Workerのコードを /src/worker/index.ts
で管理してpublic以下に生成することができたりする。
Google製のWorkBoxなるライブラリを使っていてService Workerの各種機能を簡単な記述で使えるようになるみたいですが、今回はオフライン対応などは不要だったのでtoo muchだと判断し導入を見送りました
Discussion