ラーメン画像をプッシュ通知で送りつけあって飯テロするクソWebアプリを作った



## 作ったもの

<https://ramen-sender.vercel.app>

https://ramen-sender.vercel.app?1

## どんなアプリ？

* プッシュ通知を購読し、プッシュ通知送信ボタンを押すとプッシュ通知を購読している**自分も含めたユーザ全員に**ラーメン画像のプッシュ通知を送りつけることが出来ます。
* また、飯テロといえば深夜なので毎日24時にラーメン画像がプッシュ通知で送られてきます。
* 送られてくるラーメン画像はすべて私が撮ったものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154239/da805f08-2848-f073-ac81-b9413ca381ca.png)


## なんで作ったの？

iOSのsafariのPWAでもPush通知ができるようになる！
iOS16で実装される！
みたいな情報があったから、iOS16出たしなんか作ってみよう！って思ってたんだけど
実際に使えるようになるのは2023年中(予定)らしい。

はい解散！

ではなく、その代わり？というか前段なのかな、MacOS 13 Venturaのsafari16でプッシュ通知が対応した[^1]らしい！っていうのを見つけたので試してみた。
ただサンプル作るだけじゃあんまり面白くないので、Webアプリを作った。
[^1]: 「プッシュ通知に対応した」というのは正確には誤りで、もともと独自の方法でプッシュ通知できたけどWeb標準に対応してなかったとのこと。

## この記事の対象読者

* Webアプリでのプッシュ通知をどう作るのか知りたい人
* プッシュ通知の動くサンプルがほしい人
* safariでのプッシュ通知が本当に動くのか気になる人・動かしてみたい人
* **飯テロされたい・したい人**

## やらないこと・書かないこと

* Firebase使いません
    * ネイティブアプリと統合しないなら使う必要なさそうだったので
* VAPIDやWeb Push Protocolなどの細かい説明はしません
    * とりあえず雰囲気を理解できることを目指して
* UIとかの細かい実装については書きません
    * 蛇足なので

## 全体構成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154239/181a6135-ddd0-b4c4-e5c2-0061f245374b.png)


こんな感じ
↓で説明

## 処理の流れ


1. まず、プッシュ通知の購読を開始します。購読を開始すると、PushServiceはエンドポイントとトークンを含むPushSubscriptionを返却します。(このエンドポイントにリクエストを送るとブラウザにプッシュ通知が届きます)
1. PushSubscriptionをサーバに送ります。サーバは送られてきたPushSubscriptionをDBに保存します。
1. ブラウザからプッシュ送信ボタンを押しプッシュ送信APIを実行すると、プッシュ送信APIはDBに保存されているPushSubscriptionのエンドポイント全てに対して、CMSに保存されたラーメンの画像を添付しプッシュ通知を送信します。
1. cron-jobで毎日24時にプッシュ送信APIを実行します。 [これ](cron-job.org)使ってます


## PushServiceとはなんぞや？

構成の中で出てきたPushServiceというものがあるのですが、これは各ブラウザベンダーが提供しているpush用のサーバのことです。自分たちでなにか考えることはありません。
PushSubscriptionに含まれるエンドポイントはこのPushServiceのサーバのエンドポイントです。例えばchromeなら　`https://fcm.googleapis.com/fcm/send/xxx` 、FireFoxなら `https://updates.push.services.mozilla.com/wpush/v1/xxx` みたいになってます。
ブラウザごとにプッシュ通知を出してくれるサーバがあると思ってくれればOKです。

## フロントの実装（主要な部分）

### VAPIDKeyの作成

VAPIDKeyを作ります。公開鍵と秘密鍵ができます。公開鍵はフロントとサーバ両方で使います。秘密鍵はサーバで使います。
web-pushというライブラリを使うと楽です。

```typescript
import webpush from 'web-push';
console.log(JSON.stringify(webpush.generateVAPIDKeys()));
```

### Service Workerの登録

onloadの処理とかで呼び出します。  

``` js
const registerServiceWorker = async () => {
  if ("serviceWorker" in navigator) {
    try {
      await navigator.serviceWorker.register("./svc_worker.js");
    } catch (err) {
      console.log(`Service Worker registration failed: ${err}`);
    }
  }
};
```

svc_worker.jsにはpush通知が実行されたときの処理を書きます。
ServiceWorkerのjsはrootにないと失敗するっぽいので注意です。

```js
// PushServiceからpush通知が来るとここが実行される
self.addEventListener("push", (event) => {
  // 通知設定が行われているかをチェック
  if (!self.Notification || self.Notification.permission !== "granted") {
    // 通知設定が行われていなければ何もせず終了
    return;
  }

  // 送信されたデータを取得
  if (event.data) {
    const data = JSON.parse(event.data.text());

      event.waitUntil(
      // push通知を実際に表示するところ
      self.registration.showNotification("Push Notification", {
        body: data.body,
        image: data.image,
        // imageに対応してる環境あんまりないのでiconにも同じの出す(後述)
        icon: data.image,
      })
    );
  }
});


```

### プッシュ通知購読の実装

プッシュ通知を購読する処理を書きます。サーバにPushSubscriptionを送ることでサーバからプッシュ通知を送れるようにします。

```js
  // プッシュ通知の権限をリクエストする
  Notification.requestPermission(async (permission) => {
    if (permission === "granted") {
      if ("serviceWorker" in navigator) {

        const options = {
          userVisibleOnly: true,
          // VAPIDの公開鍵を設定
          applicationServerKey: urlB64ToUint8Array(publicKey),
        };

        // Push通知を購読し、PushServiceからPushSubscriptionを取得する
        const registration = await navigator.serviceWorker.ready;
        const pushSubscription = await registration.pushManager.subscribe(options);

        // PushSubscriptionをサーバに送る
        fetch("/api/pushSubscription", {
          method: "POST",
          headers: {
            Accept: "application/json",
            "Content-Type": "application/json",
          },
          body: JSON.stringify(pushSubscription),
        });
      }
    }
  });
```

プッシュ通知送信ボタンの実装とかは蛇足なので割愛

## サーバ側の実装(主要な部分)


### プッシュ通知の送信

DBから取得とかCMSから取得とかは蛇足なのでプッシュ通知に関わる部分を抜粋。

```js
  // web-pushというライブラリを使う
  import webPush from "web-push";

  // さっき作った公開鍵と秘密鍵を設定しておく
  webPush.setVapidDetails(
    "mailto:hoge@example.com",
    process.env.VAPID_PUBLIC,
    process.env.VAPID_PRIVATE
  );

  ...

  // ブラウザからもらったpushSubscriptionを設定してsendNotificationすれば通知が送れる
  webPush
    .sendNotification(
      pushSubscription,
      JSON.stringify({
        body: message,
        image: url,
      })
    )
};

```

## 動いた！

safariさん、通知は出るけど画像は対応してないんじゃが...飯テロにならんのじゃが...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154239/cebb059e-a161-4062-70e0-5616e5e983a5.png)

macのChromeはデフォだと画像はアイコンでしか見れないけど広げるとまあまあでかくなる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154239/58e7222e-e940-a801-3054-f0668eb8f014.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154239/52fa3e98-7cf6-4e3f-b13f-c8bc3a97b384.png)


AndroidのChromeはわりといい感じ
<img width="400" alt="android" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154239/f3f7d653-80fd-70b2-348c-08e63a1b8288.jpeg">



Windowsでも動くし、EdgeとFirefoxは行けるはず
iOSは全部動かないはず


## コード

https://github.com/7tsuno/ramen-sender

Vercelと相性がいいのが楽だったのでNextでつくりました

## プッシュ通知来ないんじゃが！ってひと

ブラウザの設定か端末の設定のなにかでオフになってるかも
macのChromeだとシステム環境設定→通知→Chromeがオフになってると通知こないのでオンにしてください

あと、ブラウザによってはブラウザ落としてるときはプッシュ通知来ないかも

おやすみモードとか集中モードでも来ないかも(深夜に送るというコンセプトが...)

## 参考にした記事
https://qiita.com/murasuke/items/b1df2f72e75524a816ed

プッシュ通知の記事は大体「まずはFirebaseを用意します」みたいなのが多くて？？？ってなってたのですが、このサンプルはFirebase無しで作ってるのですごく助かった記事です。

## おわりに

大体深夜のテンションで書いたしつくった。
最近はラーメン屋に行ってお酒を飲むようになりました。

肝臓の数値がやばいのでしばらく控えたいです。

お腹へっちゃうので、むやみにプッシュ通知を送ってこないでくださいね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154239/b5e6639b-f19f-d083-6b93-00b6f4cfb8be.png)

おしまい