少し前に、Deno で Web Push が動くかと試みたら、動作せず。Issueを出していました。
npm:web-push not working
しばらく経ち、最近修正がされて、動作確認が取れました。
改めて、動作確認がてら導入方法を記しておきます。
参考
導入前準備 web-push を使うにあたり、鍵情報の発行が必要です。
ローカルで以下のコマンドを実行して、鍵情報を発行します。
1 2 $ deno -E npm:web-push generate-vapid-keys --json {"publicKey" :"hogehogehogehogehogehogehogehogehogehogehogehogehogehogehogehogehogehoge" ,"privateKey" :"hogehogehogehogehogehogehogehogehogehogehogehoge" }
この内容は、.env ファイルに記述します。
実装 実装の中で、用意すべき事項がいくつかあるので、分割して紹介します。
サーバー側実装 まず、エントリポイントにもなる main.ts です。 こちらがサーバー本体にもなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 import "@std/dotenv/load" ;import { type Route , route, serveDir } from "@std/http" ;import { getClientMainTs } from "./getClientMainTs.ts" ;import { publicVapidKey, sendNotification } from "./webPushServer.ts" ;import { registrySubscribe, subscribeKeys } from "./kvStorage.ts" ;const routes : Route [] = [ { method : "POST" , pattern : new URLPattern ({ pathname : "/subscribe" }), handler : async (request : Request ) => { const subscription = await request.json (); if (!(await registrySubscribe (subscription))) { console .error ("Failed to subscribe" , subscription); return new Response ("Failed to subscribe" , { status : 500 }); } return new Response ("Subscribed" ); }, }, { method : "GET" , pattern : new URLPattern ({ pathname : "/main.js" }), handler : () => new Response (getClientMainTs (publicVapidKey), { headers : { "Content-Type" : "text/javascript" , }, }), }, { method : "GET" , pattern : new URLPattern ({ pathname : "/*" }), handler (req ) { return serveDir (req, { fsRoot : "public" , }); }, }, ]; function defaultHandler (_req: Request ) { return new Response ("Not found" , { status : 404 }); } Deno .cron ("push message" , "* * * * *" , async () => { console .log ("Pushing message" ); const keys = await subscribeKeys (); for await (const key of keys) { await sendNotification (key, "Hello from Deno!" ); }; }); const handler = route (routes, defaultHandler);export default { fetch (req ) { return handler (req); }, } satisfies Deno .ServeDefaultExport ;
キー情報の保存を行う Deno.KV にかかわる処理、Web-push にかかわる処理、クライアント側JSの生成を行う処理は、それぞれ別ファイルに分割しています。
クライアント側JS生成 クライアント側JS生成などと銘打ちつつも、公開鍵の埋め込みだけが実態です。 埋め込みされたこのJSがクライアントで動作します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 export function getClientMainTs (publicKey: string ): string { return ` function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } (async function () { if ('serviceWorker' in navigator) { const register = await navigator.serviceWorker.register('service-worker.js'); const subscription = await register.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array("${publicKey} "), }); await fetch("/subscribe", { method: "POST", body: JSON.stringify({subscription, id: "1"}), headers: { "Content-Type": "application/json", }, }); } })(); ` ;}
Web-push にかかわる処理 Web-push にかかわる処理は、以下のようになります。 webpushの送信と、カギ情報の公開を担います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import webpush from "web-push" ;import { Subscription } from "./type.ts" ;export const publicVapidKey = Deno .env .get ("PUBLIC_VAPID_KEY" )!;const privateVapidKey = Deno .env .get ("PRIVATE_VAPID_KEY" )!;export async function sendNotification ( subscription: Subscription, payload: string , ) { webpush.setVapidDetails ( `mailto:${Deno.env.get("DOMAIN" )!} ` , publicVapidKey, privateVapidKey, ); await webpush.sendNotification (subscription, payload, { headers : { "Content-Type" : "application/octet-stream" , "Content-Encoding" : "aesgcm" , }, contentEncoding : "aesgcm" , }); }
Deno.KV にかかわる処理 クライアントから送り込まれたsubscription情報を保存/参照する処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { Subscription } from "./type.ts" ;const kv = await Deno .openKv ();export async function registrySubscribe ( subscription: Subscription, ): Promise <boolean > { return (await kv.set (["subscription" , subscription.keys .auth ], subscription)).ok ; } export async function subscribeKeys ( ) { const entries = await kv.list <Subscription >({ prefix : ["subscription" ] }); const keys = []; for await (const entry of entries) { keys.push (entry.value ); } return keys; }
フロント側実装 フロント側実装は、以下のようになります。 なんということはないです。
public/index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!DOCTYPE html > <html lang ="ja-jp" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <link rel ="stylesheet" href ="/css.css" /> <link rel ="manifest" href ="/manifest.json" /> <title > PWA Test</title > </head > <body > PWA Test </body > <script type ="module" src ="/main.js" > </script > </html >
サービスワーカー実装 サービスワーカーは次のようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const cacheFiles = ["index.html" ];const cacheName = "v1" ;self.addEventListener ("install" , (_event ) => { caches.open (cacheName).then ((cache ) => cache.addAll (cacheFiles)); }); self.addEventListener ("activate" , (_event ) => { }); self.addEventListener ("push" , (e ) => { const title = e.data .text (); self.registration .showNotification (title, { body : "web push test" , icon : "/icon-192x192.png" , }); });
動作確認 以下コマンドで、起動します。
1 $ deno serve -ERN --unstable --watch .\main.ts
ページの表示は、以下のようになります。
ここからいささか条件がはっきりしない部分があります。 何度かアクセスしていると、通知を許可するかどうかのダイアログが出てきます。 こちらは、キャプチャできませんでした。
明示的には、ChromeならブラウザのURLのところをクリックして、通知を許可するかどうかを選択できます。
許可すると、次のように通知が1分毎表示されます。(実運用で1分毎ということはないでしょうが)
メッセージを送り続けていると、通知が上がらなくなります。(それこそ1分毎送ったのがよくない可能性はあります。)
また、ブラウザで通知を停止すると、サーバー側で次のようなエラーが出ます。
1 Exception in cron handler push message WebPushError: Received unexpected response code
エラーが出たら、しばらくは通知を送らないなどの対応が必要だと考えられます。
ということで、Deno で Web Push の動作を確認しました。 しばらく対応されないことを覚悟していたところもありましたが、無事動作するようになりました。
Web push で近々何か作りたいところです。
では。