PWA の更新を分かりやすく

## 概要

[PWA](https://developer.mozilla.org/ja/docs/Web/Progressive_web_apps) を (サーバ側で) 更新した時に、ユーザから見て更新の有無や更新方法が分かりにくいと思い、分かりやすくする方法を調べました。
調べた結果、私が実践している方法をご紹介します。

||
|--|
|執筆時点ではこの方法でまあまあ上手くいってますが、私の知らない問題があるかも知れません。或は、もっといい方法を思いつくかも知れません。<br>上手く行かないケースとしては、iPhone (Mobile Safari) で PWA を再起動しても SW が更新されないことがあります。|

### 対象読者

簡単な PWA を作ったことがある人や、PWA において Service Worker がどんな働きをするか知っている人が対象です。

### 前提

この記事の中の PWA は、以下のような構造とします。

```
/
┣━ index.html
┣━ manifest.json
┣━ sw.js
┗━ scripts/
　　┗━ app.js
```

## PWA の更新 == SW の更新

PWA を更新するとは、Service Worker (以下、SW) を更新することだと考えるようにしています。
逆に言うと、SW を更新せずに他の変更 (サーバ側) を (インストール済みの) PWA に反映しようとするのは、イレギュラーなやり方です。
PWA のリソースは SW によってキャッシュされているはずだからです。

PWA (というかブラウザ) が、どういうタイミングで SW の更新をチェックするかは、こちらの記事で詳しく説明されています。

- [Service Worker スクリプトのインストールと更新処理](https://nhiroki.jp/2018/02/15/service-worker-install-and-update-scripts)

### PWA の更新 == SW のキャッシュ名の更新

サーバ上の SW が1文字でも変更されていれば、ブラウザからは更新があったと見なされます。
SW の処理を変更する必要がなくても、SW 内で定義しているキャッシュ名を変更すれば更新と見なされますし、リソースの更新を反映する際にはキャッシュ名を変えるのが常套手段です。

「PWA 開発入門」という本でも推奨されていたので、キャッシュ名にバージョン番号をつけるようにしています。

```JavaScript:sw.js
var cacheName = 'pscv 0.0.1';

// 以下の処理でこのキャッシュ名を使う
```

これと連動する形で "app.js" 内でもバージョン番号を定義するようにして、コンテンツ内 (設定画面など) で見れるようにします。

```JavaScript:app.js
const appVersion = '0.0.1';

window.onload = (event) => {
  document.getElementById('appVersion').innerHTML = appVersion;
};
```

これで、更新後の SW が動いているか (= キャッシュされているのが更新後のリソースか) が分かるようになりました。

※バージョンを上げたら古いキャッシュは削除されるような仕組みも必要です。

## SW 登録時に更新を知らせる

"index.html" 内のスクリプトで SW を登録するようにしていますが、これを以下のようにすることで、更新があることをユーザに知らせます。
PWA の起動時など、"index.html" をロードした時に更新を知ることができます。

```html:index.html
<script type="application/javascript">
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.getRegistration()
  .then (registration => {
    // 登録中の SW がなければ、これが初回登録である
    const firstRegistration = (registration === undefined);
    // SW を登録する
    navigator.serviceWorker.register('sw.js')
    .then(registration => {
      // 初回登録でなければ更新が見つかったかチェックする
      if (!firstRegistration) {
        registration.addEventListener('updatefound', () => {
          const installingWorker = registration.installing;
          if (installingWorker != null) {
            installingWorker.onstatechange = e => {
              if (e.target.state == 'installed') {
                // registration.unregister();  // 効果が疑わしいので保留
                alert('更新がインストールされました。台本ビューアを再起動してください。');
              }
            };
          }
        });
      }
    });
  });
}
</script>
```

### ざっくりとした解説

[ServiceWorkerContainer.register()](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerContainer/register) が返す Promise には [ServiceWorkerRegistration](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration) オブジェクトが来るので、これに [updatefound イベントリスナ](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration/onupdatefound) を追加しています。
updatefound イベントが発火したということは、インストール中の新しい SW があるということなので、その新しい SW の `state` が `installed` になったところで、アラートダイアログを出す等して更新を知らせます。

#### 2021-12-12 追記

SW 初回登録時に「更新がインストールされました。…」が出ないように、登録中の SW がないかチェックするようにしました。

#### 2022-02-12 追記

iPhone (Mobile Safari) で PWA を再起動しても SW が更新されない問題があったため `registration.unregister()` をしていましたが、効果が疑わしいのでコメントアウトしました。

||
|--|
|上記の仕組みを入れていると、ブラウザでのデバッグ時にロードのたびに「更新がインストールされました。…」が出る場合があります。ブラウザの開発ツールで "Update on reload" をチェックしているとそうなるようです。|

## コンテンツ内に更新チェック用のボタンを置く

コンテンツ内に更新チェック用のボタンを置いて、クリックしたら以下の関数を実行するようにします。

```JavaScript:app.js
function reload() {
  if (!('serviceWorker' in navigator))
    return;

  navigator.serviceWorker.getRegistration()
  .then(registration => {
    if (registration.waiting != null) {
      // registration.unregister();  // 効果が疑わしいので保留
      alert('インストール済みの更新があります。アプリを再起動してください。');
      disableUpdateButton();
    }
    else {
      registration.update()
      .then(registration => {
        const installingWorker = registration.installing;
        if (installingWorker != null) {
          installingWorker.onstatechange = e => {
            if (e.target.state == 'installed') {
              // registration.unregister();  // 効果が疑わしいので保留
              alert('更新がインストールされました。アプリを再起動してください。');
              disableUpdateButton();
            }
          }
        }
        else {
          alert('更新はありませんでした。');
        }
      });
    }
  });
}

function disableUpdateButton() {
  // 更新チェック用のボタンを無効化する。
  // ボタンの近くに「再起動してください。」などと表示すると良い。
}
```

### ざっくりとした解説

ServiceWorkerRegistration オブジェクトを取得し、もし [waiting プロパティ](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration/waiting) があれば、インストール済み (だが有効化されていない) 新しい SW があるということです。
なければ [ServiceWorkerRegistration.update()](https://developer.mozilla.org/ja/docs/Web/API/ServiceWorkerRegistration/update) で更新を試みて、インストール中の新しい SW があれば (登録時と同じ要領で) `state` が `installed` になったタイミングで更新を知らせます。

#### 2022-02-12 追記

新しい SW がインストールされてしまうと `registration.waiting` や `registration.update()` によるチェックが効かなくなるようなので (ボタンを押しても何も言わなくなるので)、その場合は更新チェック用のボタンを無効化すると分かりやすそうです。  
上記コードには具体的な実装は書いていませんが、`disableUpdateButton()` でそれをしている想定です。  
"index.html" 内のスクリプトも同様に変更します。