Nuxt3のメタバースサービスにPWAを導入した話

この記事はVR法人HIKKY Advent Calendar 2023、15日目の記事です。

https://qiita.com/advent-calendar/2023/hikky

昨日の記事は@otsuka_kenchanさんの「[Railsで開発を行うときに考えていること](https://qiita.com/otsuka_kenchan/items/27168d5f3964ccca12c5)」でした。

フロントエンドしかわからない自分にはオフチョベットしたテフを（以下略）でした。Railsに触れる方はぜひ。

## はじめに
みなさん、PWAしてますか？

VR法人HIKKYでフロントエンドのリードエンジニアをしています、伊織です。

先日、Nuxt3で実装されている「My Vket」というサービスにPWA(Progressive Web Apps)を導入しました。

My Vketは*Webブラウザ上で誰もが気軽に楽しめるメタバースサービス*です。3D空間に自分の部屋を作ったりできます。スマホブラウザで動きます。すごいね。

https://vket.com/

この記事では「なぜブラウザメタバースにPWAを導入したか？」やNuxt3でのPWA導入手順と課題を書きます。
解説記事というよりほぼ回想録です。

※PWAやServiceWorkerそのものの解説は省略します

## PWA導入の動機
My Vketはスマホブラウザでもアクセスできるメタバースサービスで、このようなトップページになっています。

<img width="350" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/768115a8-4bef-24a2-af84-91fad255be54.png" alt="My Vketトップページ">


あまりにもかわいいアバターに目を奪われてしまったかもしれませんが、実はこのトップページ（このサービス）には
- トークルーム
- イベント
- SNS機能（フォロー、DM）

が含まれており、これらのプッシュ通知が送りたいと要望が出てきました。
メタバースにコミュニケーションは欠かせませんから、プッシュ通知は欲しいですよね。

これまでスマホ端末へのwebプッシュはAndroid限定でしたが、2023/03に配信されたiOS16.4でiOSでも対応されました。

https://support.apple.com/ja-jp/HT213407

> ホーム画面に追加したWeb Appの通知

ただし上記にあるように「**ホーム画面に追加したWeb App**」への対応であり、ホーム画面に追加（＝PWAインストール）しないと通知が飛びません。

ということで、スマホでもプッシュ通知を飛ばすためにPWAを導入することになりました。

今回はスコープ外として実装しませんでしたが、メタバースのレンダリングに必要なファイルをキャッシュできるとローディングが早くなって快適になります。

元々のデザインもアプリっぽいしPWAになると快適そう！PWA最高！

## Nuxt3へのPWA導入
### @vite-pwa/nuxtのインストール
Nuxt3でのPWAには[@vite-pwa/nuxt](https://github.com/vite-pwa/nuxt)を利用しました。
入れるだけでServiceWorkerもmanifestファイルも作ってくれる優れもの。

https://github.com/vite-pwa/nuxt

READMEに記載の通り、インストールしてmoduleに追加します。

```bash
yarn add @vite-pwa/nuxt -D
```
```ts:nuxt.config.ts
import { defineNuxtConfig } from 'nuxt/config'

export default defineNuxtConfig({
  modules: [
    '@vite-pwa/nuxt'
  ],
  pwa: {
    /* PWA options */
  }
})
```

`PWA options`の設定については公式のサンプルが参考になりました。
manifestファイルもoptionとして定義します。

https://github.com/vite-pwa/nuxt/blob/main/playground/nuxt.config.ts


### PWAの設定

`@vite-pwa/nuxt`を入れるとServiceWorkerが自動生成されます。

大変便利なのですが、ここで3つの課題が出てきます。

1. ServiceWorkerを自動更新にしたい
1. プッシュ通知の実装どうする？
1. キャッシュをしない設定はある？

#### ServiceWorkerを自動更新にする

`registerType: 'autoUpdate'`とすると、クライアントサイドでも自動で更新が反映されるようになります。


```ts
VitePWA({
  registerType: 'autoUpdate'
})
```

https://vite-pwa-org.netlify.app/guide/inject-manifest.html#auto-update-behavior

#### プッシュ通知の実装どうする？

ライブラリを使う or 自前で実装する。

今回は自前で実装しました。
ServiceWorkerの自動生成を止めて自前のServiceWorkerを使います。

```ts
VitePWA({
  strategies: 'injectManifest',
  srcDir: 'service-worker',
  filename: 'sw.ts'
})
```

https://vite-pwa-org.netlify.app/guide/inject-manifest.html#advanced-injectmanifest

ServiceWorkerにプッシュ通知のイベントリスナーを定義します。

```ts:src/service-worker/sw.ts

declare const self: ServiceWorkerGlobalScope
self.__WB_DISABLE_DEV_LOGS = true

// プッシュ通知を受信するイベント
self.addEventListener('push', (event) => {
  // event.dataはサーバーサイドから送られてきた通知データのこと
  const data = event.data?.json()
  if (!data) return

  event.waitUntil(
    self.registration.showNotification(data.title, {
      body: data.body,
      data: {
        link: data.link,
      },
      image: data.image,
    })
  )
})

// 通知を押下したときのイベント
self.addEventListener('notificationclick', (event) => {
  const link = event.notification.data.link
  if (!link) return
  self.clients.openWindow(link)
})
```

#### キャッシュをしない設定はある？

ある。

https://vite-pwa-org.netlify.app/workbox/inject-manifest.html#network-first-strategy

サンプルを参考にｺﾞﾆｮｺﾞﾆｮします。

```ts:src/service-worker/sw.ts
import { cleanupOutdatedCaches } from 'workbox-precaching'
import { clientsClaim } from 'workbox-core'
import { NetworkOnly } from 'workbox-strategies'
import { registerRoute, setDefaultHandler } from 'workbox-routing'
import type { ManifestEntry } from 'workbox-build'

declare const self: ServiceWorkerGlobalScope
self.__WB_DISABLE_DEV_LOGS = true

cleanupOutdatedCaches()
// 全てのリクエスト(URL)に対してNetworkOnlyを設定
registerRoute(({ url }) => {
  ;(self.__WB_MANIFEST as Array<ManifestEntry>)
    .map((entry) => new URL(entry.url, self.location.origin).href)
    .includes(url.href)
}, new NetworkOnly())

self.skipWaiting()
clientsClaim()

setDefaultHandler(new NetworkOnly())
```

## 動作確認編

スマホ実機で動作確認をする際のハマりポイントを紹介します。

### ベーシック認証をかけているとiOS Safariでアイコンが出ないし動作確認もできない

| ベーシック認証なし | ベーシック認証あり |
|:-:|:-:|
| ![myvketdev.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/f28f0055-1d6c-ba7e-f36d-755060dc1e1f.png)  | ![myvketstg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/1986442e-706e-c608-a9d4-f3fab1285c15.png)  |

ベーシック認証ありだと起動後も真っ白。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/8d8e8d85-017c-0a7d-9af4-5c9764768c41.png">


設定はしているはずなのに検証環境（ベーシック認証あり）でアイコンが出ず、そこそこの時間悩んでしまいました。

厄介だったのが、Android Chromeではベーシック認証ありでもアイコンも内容表示もできていたことです。
Safari～～～～😇

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/5502a947-0bab-c5b2-837a-ffbac10c4468.png">

### iOS SafariのNotification APIはPWAインストール後にしか存在しない

通知許可をブラウザに求めるため`Notification.requestPermission()`を実行するのですが、iOS SafariではPWAインストール後でないとNotification APIが存在しません。

https://developer.mozilla.org/ja/docs/Web/API/Notification

そのため、iOSであるかどうか + PWAで開いているかどうかのチェックを行い、iOSではPWAのときだけ`requestPermission()`を実行します。

```ts
if (!isIOS() || (isIOS() && isPwa())) {
    Notification.requestPermission((_permission) => {
        if (_permission === 'granted') {
            // サーバーサイドとの連携処理
        }
    })
}
```

Safari～～～～😇😇

## 今後の課題

### マルチオリジンのPWA

今回の実装でMy Vketのマイルームをアプリっぽくすることができました。
触ってみるとよくわかるのですが、ブラウザ版と比べてかなり体験が良くなっています。

一方、ワールドへ遷移するとアプリ内ブラウザが立ち上がり、表示領域が狭くなってしまっています。
| マイルーム | ワールド |
|:-:|:-:|
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/c0d63d7c-ce6a-5089-3d5e-69db9a1d32a8.png)  | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/64f57fb5-db7b-ac3e-47b8-01536c1c57b6.png)  |

これはマイルームとワールドでoriginが異なることが原因です。
今後、My Vketと関連サービスをアプリ内でシームレスに行き来できるよう、マルチオリジン対応（もしくは同一オリジンにまとめていく対応）を進めていきます。

参考：

https://web.dev/articles/multi-origin-pwas?hl=ja

### キャッシュ

今回の実装ではキャッシュ対応はしませんでした。
適切なキャッシュをすることで3Dの描画速度を上げ、UXの向上を進めます。

## まとめ
ブラウザメタバースというWebプッシュ通知の需要が高いサービスにPWAとプッシュ通知を実装しました。
これによりアプリと同等の操作感と通知機能が得られ、より良いサービスになったと思います。

今後も改善を進め、ネイティブアプリに負けないUXを提供できるよう頑張ります。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3649028/1b0c9adf-3f70-2310-f0be-89fe7422042a.png">

（ぞい）