そこそこ小さくまとめたVAPID Web Push通知デモ(Node.js)

Web のお仕事をしなくなって、ここ最近 Web のキャッチアップがめっきり減っている今日この頃。
Web のお仕事に携わってた頃は、FCM を使った WebPush の実装は経験があるのですが、FCM を使わずにできるようになっているということで、リハビリがてら VAPID での Web Push API を改めて実装してみました。

```html:index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>VAPID WebPush Demo</title>
</head>
<body>
    <button id="btnWebPushTest">プッシュ通知テスト</button>
    <script src="js/vapid_demo.js"></script>
</body>
</html>
```

```js:vapid_demo.js
let convertedVapidKey, subscription;

(async _ => {
    try {
        // サービスワーカー登録
        const registration = await navigator.serviceWorker.register('/sw.js', { scope: '/' });

        // サーバー側で生成したパブリックキーを取得し、urlBase64ToUint8Array()を使ってUit8Arrayに変換
        const res = await fetch('/key');
        const vapidPublicKey = await res.text();
        convertedVapidKey = urlBase64ToUint8Array(vapidPublicKey);
        
        // (変換した)パブリックキーをapplicationServerKeyに設定してsubscribe
        subscription = await registration.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: convertedVapidKey
        });
        
        // 通知の許可を求める
        Notification.requestPermission(permission => {
            console.log(permission); // 'default', 'granted', 'denied'
        });
    } catch (err) {
        console.log(err);
    }
})();

btnWebPushTest.onclick = async evt => {
    if (!subscription) return console.log('sbuscription is null');
    await fetch('/webpushtest', {
        method: 'POST',
        body: JSON.stringify(subscription),
        headers: {
            'Content-Type': 'application/json',
        },
    });
};

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;
}
```
```js:sw.js
  
self.addEventListener('push', evt => {
    const data = evt.data.json();
    console.log(data);
    const title = data.title;
    const options = {
        body: data.body,
        icon: 'test.jpg'
    }
    evt.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener('notificationclick', evt => {
    evt.notification.close();
});
```
```js:server.js(Node.js)
const Koa = require('koa');
const Router = require('koa-router');
const serve = require('koa-static');
const koaBody = require('koa-body');
const webPush = require('web-push');

const app = new Koa();
const router = new Router();
const vapidKeys = webPush.generateVAPIDKeys();

webPush.setVapidDetails(
    'mailto:hoge@fuga.piyo', // 第一引数は'mailto:～'というフォーマットでないとだめらしい
    vapidKeys.publicKey,
    vapidKeys.privateKey
);

router
    .get('/key', ctx => {
        ctx.body = vapidKeys.publicKey;
    })
    .post('/webpushtest', koaBody(), async ctx => {
        try {
            setTimeout(_ => { // ちょっと遅延させて通知
                await webPush.sendNotification(ctx.request.body, JSON.stringify({
                    title: 'Web Push通知テスト',
                }));
            }, 5000);
        } catch (err) {
            console.log(err);
        }
    });

app
    .use(serve(__dirname + '/public'))
    .use(router.routes())
    .use(router.allowedMethods());

app.listen(3000);
```

```txt:ファイル配置
app
├── public
│   ├── js
│   │   └── vapid_demo.js
│   ├── sw.js
│   └── index.html
└── server.js
```

面倒なところは、Koa使って片付けてます。
以前にも使っていた[web-push](https://github.com/web-push-libs/web-push#readme)というNode用のWeb PushライブラリがVAPIDにも対応しているということでこれを使いました。
ページにあるボタンをクリックするとサーバーから通知を送るようにしていますが、遅延して通知するようにしていますので、ボタンクリックした後にブラウザー閉じても通知を受け取ることができるかどうかのテストができます。
コードは一応、GitHubにも上げました。
[VAPID_WebPush_Demo](https://github.com/gtk2k/VAPID_WebPush_Demo)

### ちょっとはまったところ
webPush.sendNotification() を実行すると
`the key in the authorization header does not correspond to the sender ID used to subscribe this user. Please ensure you are using the correct sender ID and server Key from the Firebase console.`
というエラーが発生し、え？Firebase? とちょっと焦りましたが、これはクライアント側のサービスワーカーが更新されていないのが原因で、devTools使ってサービスワーカーをいったん削除して登録しなおしたらうまくいきました。