0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Express + Service Worker で最小構成 PWA Push 通知を体験する

Posted at

アプリを作る気はなくてWebサービスで通知してかつPWAにしてクロスプラットフォームでアプリ化もできるようにしたいというモチベーションがあったので、Push通知の仕組みを理解するために最小構成で実装してみた。


PWA とは

Progressive Web Apps とは Web 技術 (HTML/CSS/JS) で作ったサイトを「インストール可能・オフライン対応・プッシュ通知対応」などアプリライクに拡張する一連のパターン / 機能群である。
コアになるのが:

要素 役割
Service Worker ネットワーク介在 / キャッシュ制御 / Push 受信 / バックグラウンド処理
Web App Manifest 名前・アイコン・起動モード (display) 等を OS / ブラウザへ宣言
HTTPS セキュアコンテキスト必須 (localhost は例外)
Push API & Notifications API サーバー発→ブラウザ→ユーザー通知

今回フォーカスするのは Push と Notification である。キャッシュや offline は割愛し最短ルートで通知を通す。

Push 通知の仕組み

  1. ブラウザが Service Worker を登録
  2. ユーザーが通知許可を与える (Notifications API)
  3. アプリ (フロント) が PushManager.subscribe() で購読を作成
    • ブラウザはベンダー毎の Push Service (FCM, WNS, etc.) に endpoint を発行
    • 公開鍵 (p256dh) と認証鍵 (auth) を含む JSON が返る
  4. その購読情報をアプリサーバーへ保存
  5. サーバーは web-push ライブラリで VAPID 署名付きの HTTP リクエストを Push Service へ投げる
  6. Push Service が対象ブラウザへ配送 → Service Worker の 'push' イベント発火
  7. Service Worker が showNotification() で OS 通知に表示

購読はブラウザ / 端末 * アプリオリジン 毎に固有である。1 ユーザーが複数端末を使えば購読は複数必要になる。

完成イメージ

シンプルな 1 ページ構成:

  • 見出し下に「通知を表示」「通知解除」ボタン
  • ステータス表示 (購読中 / 未購読)
  • コンソールに最小ログ (サブスク完了 / サブスク終了)
  • サーバー側 curl で任意タイトルと本文を即時通知

全体構成

開発環境

コンポーネント役割

コンポーネント ファイル 役割
クライアント public/main.js 許可要求 / 購読生成 / サーバー同期 / 解除
Service Worker public/sw.js push イベント受信 → 通知表示 / クリック挙動
サーバー server.js VAPID 鍵生成・購読保存・送信・解除
簡易永続化 subscriptions.json 再起動後も購読を復元

ファイル構成 (抜粋)

public/
	index.html
	style.css
	main.js
	sw.js
server.js
vapid-keys.json (開発時自動生成)
subscriptions.json (購読保存)

主要技術要素と要点

要素 ポイント
Service Worker install/activate で即時制御 (skipWaiting + clients.claim)
PushManager.subscribe applicationServerKey は URL Safe Base64→Uint8Array 変換が必要
VAPID サーバー署名で送信主体を証明 (メールアドレス or URL 指定)
web-push ライブラリ 暗号化 + VAPID 署名 + Push Service への送信を肩代わり
失効処理 410 / 404 で購読削除しリーク防止
購読再同期 既存 subscription でもサーバー再起動後に /subscribe 再送
明示的解除 unsubscribe + /unsubscribe でサーバー側も削除

実装ステップ詳もろもろ

1. プロジェクト作成パッケージ追加

npm init -y
npm install express web-push

2. VAPID 鍵生成

server.js 起動時に vapid-keys.json が無ければ 生成する。

const generated = webPush.generateVAPIDKeys();
fs.writeFileSync(keyFile, JSON.stringify(generated, null, 2));

本番は環境変数 VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY を注入する。

3. Service Worker 登録

navigator.serviceWorker.register('/sw.js?v=2');

クエリ付与でキャッシュをバストさせる。

4. 通知許可

if (Notification.permission === 'default') {
	await Notification.requestPermission();
}

5. 公開鍵取得エンドポイント

app.get('/vapidPublicKey', (_,res)=> res.json({ publicKey: vapidKeys.publicKey }));

6. 購読生成 & 再同期

const vapidRes = await fetch('/vapidPublicKey');
const { publicKey } = await vapidRes.json();
const keyBuf = urlBase64ToUint8Array(publicKey);
sub = await reg.pushManager.subscribe({ userVisibleOnly:true, applicationServerKey:keyBuf });
await fetch('/subscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(sub)});

7. サーバーで保存

/subscribe 受信 → endpoint をキーに Map + JSONとして保存。

app.post('/subscribe',(req,res)=>{
	const s=req.body; if(!s?.endpoint) return res.status(400).json({error:'Invalid'});
	if(!app.locals.subscriptionMap) app.locals.subscriptionMap=new Map();
	app.locals.subscriptionMap.set(s.endpoint,s); persistSubs();
	console.log('サブスク完了');
	console.log(`サブスク数 ${app.locals.subscriptionMap.size}`);
	res.json({status:'ok'});
});

8. 送信 /send

await webPush.sendNotification(subscription, JSON.stringify({title, body}));

失効コード 410 / 404 を受け取った場合は削除し サブスク終了 を記録する。

9. Service Worker で通知

self.addEventListener('push',e=>{
	let data={title:'Default', body:'...'};
	if(e.data){try{data=JSON.parse(e.data.text())}catch{}}
	e.waitUntil(self.registration.showNotification(data.title,{body:data.body}));
});

10. クリック挙動

既存タブをフォーカスするか新規オープンする。

11. 解除 /unsubscribe

app.post('/unsubscribe',(req,res)=>{ const {endpoint}=req.body||{}; const map=app.locals.subscriptionMap||new Map(); if(map.delete(endpoint)){persistSubs(); console.log('サブスク終了'); console.log(`サブスク数 ${map.size}`);} res.json({status:'ok'}); });

フロント側で sub.unsubscribe() 実行後にサーバーへ endpoint を POST する。

主要コード抜粋

server.js (要点)

function ensureVapidKeys(){ /* 省略 (自動生成 or 環境変数) */ }
webPush.setVapidDetails('mailto:example@example.com', vapidKeys.publicKey, vapidKeys.privateKey);
app.post('/send', async (req,res)=>{ /* 送信 & 410/404 削除 */ });

main.js 購読確立

async function ensureSubscription(){
	const reg = await navigator.serviceWorker.ready;
	let sub = await reg.pushManager.getSubscription();
	if(!sub){ /* 公開鍵取得 → subscribe() */ }
	await fetch('/subscribe',{ method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(sub)});
	console.log('サブスク完了');
	return sub;
}

購読解除

const sub = await reg.pushManager.getSubscription();
await sub?.unsubscribe();
await fetch('/unsubscribe',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({endpoint:sub?.endpoint})});
console.log('サブスク終了');

sw.js push イベント

self.addEventListener('push', event => { /* JSON 解析 → showNotification */ });
self.addEventListener('notificationclick', event => { /* 既存クライアント focus */ });

デバッグ & 確認ポイント

観点 方法
登録済購読一覧 GET /debug/subscriptions
生データ GET /debug/subscriptions/raw
手動通知送信 curl POST /send
解除後の件数 /unsubscribe 実行 → /debug/subscriptions
失効処理 endpoint を無効化 (別ブラウザで unsub) → /send

動作確認

サーバー起動

npm run dev

ブラウザで http://localhost:3000 にアクセスし、通知を許可して「通知を表示」ボタンをクリック。

image.png

クリックすると「購読中」表示される。

image.png

PCの通知も来ること確認。

続いてブラウザを閉じてサーバー側で curl で送信してみる👀

# 通知送信
curl -s -X POST http://localhost:3000/send \
	-H 'Content-Type: application/json' \
	-d '{"title":"警告","body":"PCのあらゆるデータがインターネットに公開されました💪"}'

image.png

ちゃんと通知が来た。こんな通知きたらびっくりするよね。

まとめ

割と簡単にPush通知の実装ができた。

POCなのVAPIDの管理を簡易にしているが、本番では環境変数やAWS Secrets Manager等で厳重に管理することが不可欠である。また、Service WorkerとPush APIの仕様上、localhost環境以外ではHTTPSが必須。

あとは通知のタイミングはアプリのユースケースに合わせて設計する。
例えばチャットアプリなら新着メッセージ受信時、ECサイトなら注文状況更新時などなど。

以上!!

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?