Svelte で PWA (台本ビューア) <第6回>

[PWA で作った台本ビューア](https://github.com/satamame/pscv/tree/0.0.2) を [Svelte](https://svelte.jp) で作り直すことにしたので、やったことをメモしていきます。

- 前回 : [<第5回> データを読込む](https://qiita.com/satamame/items/529b016e95f6d412dbad)
- 次回 : [<第7回> 目次によるジャンプ](https://qiita.com/satamame/items/27439b805e3a0cccdcc7)
- [シリーズ記事一覧](https://qiita.com/satamame/items/3e0683a1fc6ecd6b66ee#%E3%82%B7%E3%83%AA%E3%83%BC%E3%82%BA%E8%A8%98%E4%BA%8B%E4%B8%80%E8%A6%A7)

# <第6回> 更新の動作の改善

**今回の概要**

- アプリを更新できない時があったので、その問題について考察します。
- ブラウザで実行している時は更新のプロンプトを出さないようにします。
- 更新の動作について注意書きで補足します。

今回使うソースコードは [GitHub のこちらのコミット](https://github.com/satamame/pscv/tree/qiita.06) にあります。

# アプリを更新できない問題

[<第3回>「更新プロンプトを出す」](https://qiita.com/satamame/items/36c6761d363ca3894824) で Service Worker の更新について実装しましたが、その後、更新プロンプトの「更新する」ボタンを押しても何も起こらないことがありました。

結論からいうと、この問題は [vite-plugin-pwa](https://vite-plugin-pwa.netlify.app/) をアップデートすると解決しました。

![06_update_prompt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/3f5d23c5-086a-e341-def1-db563b616fa3.png)
※↑この「更新する」ボタンを押しても何も起こらないことがあった。

原因は、ブラウザでもこの PWA のページを開いて更新していたからだと分かりました。
ブラウザとインストールした PWA の両方で同じ更新をしようとした時に、この問題が起こっていたのです。
あまり起きない状況かとは思いますが、ボタンが利かなくなるのは気持ち悪いです。

## 考察

おそらくですが、ブラウザと PWA の両方で更新を検知してプロンプトを出して、一方が更新を完了してしまうと、もう一方が「更新は完了しているので何もしない」という状態になっていたのだと思います。

この時とにかくリロードするという手もあります。
`updateServiceWorker()` は `Promise` を返すので、以下のように書き換えればリロードするようになります。

```typescript
// 変更前
updateServiceWorker(true)

// 変更後
updateServiceWorker(true).then(() => { location.reload() })
```

しかし引数 (reloadPage) に `true` を指定しているのに自前でリロードをするのもおかしな話です。
そう思って vite-plugin-pwa の GitHub を見てみると、アップデートのロジックについての修正がありました。

- [Release v0.14.1 · vite-pwa/vite-plugin-pwa · GitHub](https://github.com/vite-pwa/vite-plugin-pwa/releases/tag/v0.14.1)

## vite-plugin-pwa をアップデート

これを入れてみたところ先ほどの問題は解消され、自前でリロードする必要はなくなりました。

```json:package.json
{
  (中略)
  "devDependencies": {
    (中略)
    "vite-plugin-pwa": "^0.14.1",  <- このように更新して yarn install
    (中略)
  }
}
```

具体的には、ブラウザと PWA の両方で更新プロンプトを開いて一方で「更新する」を実行すると、もう一方でもリロードが起きるようになりました。
別のアプリによって更新された場合でも、更新が完了したことを検知できるようになったのだと思います。

# ブラウザ実行の扱い

そもそもブラウザで実行している時に更新プロンプトを出すべきか、というのも気になります。
普通に Web アプリを見ていて「更新しますか？」と言われても、ユーザは何のことかと思うかも知れません。

なので、ブラウザで実行している時は更新のプロンプトを出さないことにしました。
プロンプトを出さなくともサーバ上の Service Worker の更新は監視されますし、新しくインストールされた Service Worker はブラウザを再起動すれば有効化されます。

:::note info
Service Worker の自動更新について
ブラウザがサーバ上の Service Worker の更新を確認する時、最後の更新から24時間は更新を検知しません (または24時間以内のキャッシュを参照します)。自動更新の動作を確認するにはサーバ上の Service Woreker を更新して24時間待つ必要があります。
:::

## ブラウザ実行であることを判定する

「ブラウザで実行している時は更新のプロンプトを出さない」ということを実現するために、(インストールした PWA ではなく) ブラウザで実行していることを判定する必要があります。

その方法を [PWA Night の Slack](pwanight.slack.com
) で質問したところ、以下のページを教えていただけました。

- [PWAで動いてる？isPWA();](https://dev.to/origamium/pwa-55j2)

これをそのままコピーして、lib フォルダの中に [env.ts](https://github.com/satamame/pscv/blob/qiita.06/src/lib/env.ts) を作って `isPwa` を定義しました。

```typescript:src/lib/env.ts (抜粋)
// PWA として実行しているかの判定
export const isPwa = window.matchMedia('(display-mode: standalone)').matches
```

## ブラウザ実行時はプロンプトを出さない

あとは、これを使って `isPwa` が `false` ならプロンプトを出さないようにするだけです。
App.svelte のコード全文は [こちら](https://github.com/satamame/pscv/blob/qiita.06/src/App.svelte) にあります。

```html:src/App.svelte (抜粋)
<script lang="ts">
  import { isPwa, isAndroid } from './lib/env'
  /* 中略 */
</script>

.....

{#if isPwa && reloadIsOpen}
  <ReloadPrompt
    on:close="{() => { reloadIsOpen = false }}"
  />
{/if}
```

:::note warn
上記の方法では、ブラウザで最初に実行した時に Service Worker が見つからないという問題があることが分かりました。`ReloadPrompt` コンポーネント自体をマウントしないのではなく、その中のプロンプトの表示制御でブラウザ実行かどうかを条件とすべきでした。
その改善をしたコードは [こちら](https://github.com/satamame/pscv/commit/0a3c834309ab1e3df7ba0a6def8f97037a60a867) にあります。
:::

# 注意書きでより親切にする

以上で更新の動作が改善できたと思いますが、さらに注意書きを入れることで、よりユーザに親切になると思いました。
注意書きは `About` コンポーネント (バージョン情報を表示するパネル) に以下の2つを表示することにしました。

1. 更新が途中で止まった場合について
    「更新が途中で止まった場合は、一度アプリを終了して起動しなおしてください。」
2. ブラウザで実行している場合の更新について
    「新しいバージョンは自動的に配信されます。常に最新のバージョンを使うには、このページを PWA としてインストールするか、キャッシュをクリアして再読込みしてください。」

About.svelte のコード全文は [こちら](https://github.com/satamame/pscv/blob/qiita.06/src/components/About.svelte) にあります。

```html:src/components/About.svelte (抜粋)
<div class="panel" class:gone>
  .....
  <div class="container">
    <p>バージョン<br>{APP_VERSION}</p>
    {#if $appUpdateFunc}
      <button on:click|once="{() => $appUpdateFunc(true)}">
        今すぐ台本ビューアを更新する
      </button>
      <div style="text-align: left;">
        <p>
          更新が途中で止まった場合は、一度アプリを終了して起動しなおしてください。
        </p>
      </div>
    {/if}
    {#if !isPwa}
      <div style="text-align: left;">
        <p>
          新しいバージョンは自動的に配信されます。<br />
          常に最新のバージョンを使うには、このページを PWA としてインストールするか、キャッシュをクリアして再読込みしてください。
        </p>
      </div>
    {/if}
    <p>{COPY_RIGHT}</p>
  </div>
</div>
```

更新が途中で止まった場合については、更新ボタンが表示されている時だけ表示すれば良いので、更新ボタンと同じ `if` ブロックの中に入れました。
![06_notification1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/2b56205a-033e-14ea-5fef-e8ed9e786c29.png)

ブラウザで実行している場合の更新については、ブラウザで実行している時だけ表示されるように、新しく `if` ブロックを作ってその中に入れました。
![06_notification2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/339253/727226b0-eeb8-300c-0961-e84e23676bcd.png)