Vue.jsで作ったWebアプリのPWA対応とキャッシュ問題への対策

# Vue.jsで作ったWebアプリのPWA対応とキャッシュ問題への対策

この記事は[サーバーレスWebアプリ Mosaic](https://mosaic.w2or3w.com "Mosaic")を開発して得た知見を振り返り定着させるための[ハンズオン記事](https://qiita.com/w2or3w/items/87b57dfdbcf218de91e2)の1つです。

以下を見てからこの記事をみるといい感じです。
* [AWS Amplify Consoleでビルド＆デプロイ](https://qiita.com/w2or3w/items/da1134fa3a14660dcc54)
* [お名前.comで取得した独自ドメインのサブドメインをAmplify Consoleで割り当てる](https://qiita.com/w2or3w/items/ddb7c4c1a602470ef96b)

## はじめに
Webアプリをドメイン取って公開したら、次はPWA対応もしたくなりますよね。PWA(Progressive Web Apps)とは、Webアプリをモバイルアプリのようにスマホにインストールすることができる技術です。インストールするというか、スマホのホームにアイコンが追加され、一見モバイルアプリのように動作させることができます。
Vueで作成したWebアプリなら、簡単にPWA対応を実装することができます。
この技術のアルアルとして、「キャッシュがクリアされなくてバージョンアップしたアプリが反映されない」という問題がありまして(特にiOSのSafari)、それへの対応についても書きます。

## VueプロジェクトのPWA対応
### サービスワーカーの追加とインポート
`src/registerServiceWorker.js`ファイルを追加します。

```src/registerServiceWorker.js
import { register } from 'register-service-worker'

if (process.env.NODE_ENV === 'production') {
  register(`${process.env.BASE_URL}service-worker.js`, {
    ready () {
      console.log(
        'App is being served from cache by a service worker.\n' +
        'For more details, visit https://goo.gl/AFskqB'
      )
    },
    registered () {
      console.log('Service worker has been registered.')
    },
    cached () {
      console.log('Content has been cached for offline use.')
    },
    updatefound () {
      console.log('New content is downloading.')
    },
    updated () {
      console.log('New content is available; please refresh.')
    },
    offline () {
      console.log('No internet connection found. App is running in offline mode.')
    },
    error (error) {
      console.error('Error during service worker registration:', error)
    }
  })
}
```

続いて`src/main.js`に`import './registerServiceWorker';`を追加します。

```src/main.js
import Vue from 'vue'
import App from './App.vue'
import vuetify from './plugins/vuetify';
import router from './router';
import Amplify from 'aws-amplify';
import aws_exports from './aws-exports';
import './registerServiceWorker';

Vue.config.productionTip = false
Amplify.configure(aws_exports)

export const eventBus = new Vue();

new Vue({
  vuetify,
  router, 
  render: h => h(App)
}).$mount('#app')
```

### manifest.jsonと各サイズのアイコン画像
`public/manifest.json`とアイコンを作成するのですが、これを自分の手を動かして作成しようと思うとなかなか大変です。便利なサービスがあるのでそれを利用しましょう。
https://app-manifest.firebaseapp.com/
![Screenshot 2020-01-19 at 11.49.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/66f05b38-c72a-094e-f8b1-c3f65c73caa1.png)
名前や色はお好みに指定してください。
Display ModeはStandaloneにしてください。

アイコンは「いやすとや」からダウンロードしてきたこちらを利用しました。かわいいですね。
![bluebird_robot_bot (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/4c4b86ae-c216-f1a7-8438-472f641f4b85.png)

GENERATE ZIPボタンを押下すると必要なファイルが入ったzipファイルをダウンロードすることができます。
zipの中に入っている以下のファイルをプロジェクトのpublicフォルダ以下に追加します。
manifest.json
images/icons/icon-72x72.png
images/icons/icon-96x96.png
images/icons/icon-128x128.png
images/icons/icon-144x144.png
images/icons/icon-152x152.png
images/icons/icon-192x192.png
images/icons/icon-384x384.png
images/icons/icon-512x512.png

```manifest.json
{
  "name": "SampleApplication",
  "short_name": "SampleApp",
  "theme_color": "#512da8",
  "background_color": "#ff5722",
  "display": "standalone",
  "Scope": "/",
  "start_url": "/",
  "icons": [
    {
      "src": "images/icons/icon-72x72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-96x96.png",
      "sizes": "96x96",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-144x144.png",
      "sizes": "144x144",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-152x152.png",
      "sizes": "152x152",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "images/icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "splash_pages": null
}
```

### faviconの更新
もしfaviconを変更していない場合はついでに更新しておきましょう。
icon-72x72.pngをfavicon.pngにリネームして、publicフォルダに追加するのが楽でいいと思います。もともあったfavicon.icoは削除してしまいましょう。(そのまま残しておくと悪さをしますので削除してください。）

### ライブラリのインストール
このままだとregister-service-workerが無いためビルドエラーになります。
また、@vue/cli-plugin-pwaがないとPWAがうまく登録されません。
そのため、以下の2つのライブラリをインストールする必要があります。

```
$ npm install register-service-worker
$ npm install @vue/cli-plugin-pwa
```

### 動作確認
ひとまずこれで、VueプロジェクトのPWA対応の基本設定は完了です。
デプロイしてモバイル端末でアクセスし、確認してゆきましょう。
(※Android前提で話を進めます。iPhoneの場合はちょっとワケがあり、それについては後述します。）

噂によると5分以上間隔をあけて2回アクセスする必要があるようなのですが、「ホーム画面にSampleAppを追加」というポップアップが表示されるようになりました。
![Screenshot_20200119-115242.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/baf86b93-2df0-210d-bd98-792161b740b7.png)

追加しましょう。
![Screenshot_20200119-115258.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/56f213eb-4f55-826f-de6c-10af70eb3491.png)

ホーム画面にアイコンが追加されました。
![Screenshot_20200119-115515.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/dfc51493-450e-5263-fd15-063ec1cc70c1.png)

実行すると、モバイルのネイティブアプリのように動きます。
![Screenshot_20200119-115455.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/c87c221c-a85e-15d6-2345-9d11db54e67c.png)
![Screenshot_20200119-120037.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/b5e69b35-10b2-7b46-2406-c452d755d31b.png)

### ツールバーの色がVueの緑色から変わらない問題への対応
画面の最上部、スマホの時計やバッテリーが表示される領域の色が、指定した記憶のない、しかし見覚えのある緑色になっています。そう、Vue.jsのグリーンカラーですね。
manifest.jsonのtheme_colorになってくれてもいいと思うのですが、Vueの呪いでしょうか、そうなってくれません。Vueの事は嫌いじゃないですが、ちょっとこのままだとダサいので、色を変えたいと思います。

ここの色を変えるためには、index.htmlのメタタグに指定する必要があります。

```public/index.html
  :
    <meta name="theme-color" content="#FF4081">
  :
```
分かりやすくピンクにしておきました。

キャッシュが効いてて、ブラウザの閲覧履歴を削除したりする必要があるかもしれませんが、色が変わった事を確認してみてください。
![Screenshot_20200119-192132.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/a9a735bc-2b76-7bbe-8efe-4c5fcf6c6654.png)

## ブラウザのキャッシュをクリアするために
これはPWAに限らずかもしれませんが、Webアプリの機能追加やデザイン変更を行っても、それが反映されないことが度々あります。先ほどツールバーの色を変えましたが、なかなか変更が反映されなかったのではないでしょうか。すぐに反映させたい場合にはブラウザから閲覧履歴やWebサイトデータを削除する必要があったりして、でもそれって一般ユーザーに求められることではなかったりします。

機能追加やデザイン変更を行った際、なかなかユーザーのブラウザに反映されないとしたら悲しいですよね。
そこで、バージョンアップしたらユーザーのブラウザのキャッシュをクリアするための仕掛けを用意しておきましょう。これは重要な仕掛けです。PWA対応アプリには必須でしょう。

やり方は色々とあると思うのですが、私は以下の設計で実現しています。
1. バックエンドでバージョン番号を保持する。
2. Webアプリのトップ画面を表示する際、バックエンドからバージョン番号を取得する。
　 (ワタシのアプリはサーバーレスなので、APIから取得しています。)
3. Cookieからバージョン番号を取得する。
4. Cookieからバージョン番号を取得できなかった場合や、Cookieのバージョン番号とAPIで取得したバージョン番号が異なる場合は、キャッシュをクリアして画面をリロードする。
5. APIで取得したバージョン番号はCookieに保存しておく。

```src/App.vue
  :
<script>
  :
import { API, graphqlOperation } from 'aws-amplify';
import { listSampleAppsyncTables } from "../graphql/queries";
  :
      let apiResult = await API.graphql(graphqlOperation(listSampleAppsyncTables, { group : "version" }));
      let item = apiResult.data.listSampleAppsyncTables.items[0];
      let version = item.path;
      var versionCookies = Cookies.get('sub.w2or3w.work.version');
      Cookies.set('sub.w2or3w.work.version', version, { expires: 10 });
      if(version != versionCookies){
        window.location.reload(true);
      }
  :
```
「キャッシュをクリアして画面をリロードする」のは`window.location.reload(true);`この部分です。

さぁこれで、バージョンアップ後にバージョン番号を変えてあげたら自動的にキャッシュをクリアして画面をリロードすることができ、更新後のWebアプリをすぐユーザーに使ってもらえますね！！

、、と思いきや、iOSのPWA(Safari)が手強い。`window.location.reload(true);`これだけだと更新してくれないのです。対応方法については後述します。

## iPhone(iOS)対応
今までAndroid前提で話を進めてきました。iPhone(iOS)だと期待したようならないことがいくつかありまして、それについて書こうと思います。

### ホーム画面へ追加の制限
まず、iPhoneのChromeからはホーム画面へ追加ができません。
![IMG_7223.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/ea9a4b16-7334-5f1d-c97e-9e55a3728813.png)

Safariからしかできず、また、Androidのようにポップアップは表示されずに、わざわざメニューから追加する必要があります。
![IMG_7224.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/ed0be99a-0e6c-49e1-7183-19687a576242.png)
![IMG_7225.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/264f9351-3994-e142-927a-38d070f46b5b.png)

iPhoneユーザーでもWebブラウザはChromeを利用している人は多いのではないでしょうか。ちょっとこの制限は痛いですよね。早くiPhoneのChromeでもAndroidと同じようにホーム画面へ追加できるようになってもらいたいものです。

### アイコンが表示されない
そしてSafariでホーム画面へ追加をする際、今のままだとアイコンが表示されず、なぜかアプリのスクリーンショットのようなイメージとなってしまっています。
![IMG_7226.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/3f950564-f8bf-97c7-0d50-b0d2bd0aea09.png)

アイコンを表示させるためには、index.htmlのメタタグに`apple-touch-icon`を指定する必要があります。

```public/index.html
  :
    <link rel="apple-touch-icon" href="<%= BASE_URL %>images/icons/icon-192x192.png" sizes="192x192"/>
  :
```
![IMG_7227.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/579f86e8-6e37-6680-32fd-867fc73a8bf6.png)
無事にiPhone(iOS)でもアイコンが表示されました。

### ツールバーの色が変わらない
せっかくダサいVue.jsグリーンカラーからかわいいピンクにしたのに残念です。
やり方分かってません。できないのかも。知っている人がいたら教えて下さい。

### 起動時のスプラッシュ画面が表示されない
AndroidだとPWAアプリ起動時にスプラッシュ画面が表示されてかっこいいですが、iPhoneだと表示されません。
こちらもやり方分かってません。できないのかも。知っている人がいたら教えて下さい。

### キャッシュがクリアされない
上の方「ブラウザのキャッシュをクリアするために」で書きましたが、iPhone(iOS)のPWA(Safari)は`window.location.reload(true);`これを走らせてもキャッシュがクリアされません。
これだと、せっかくアプリをバージョンアップさせても既存ユーザーにその機能を利用してもらえなくて悲しいです。

対応方法としては、ServiceWorkerのRegistrationをunregisterしてあげます。
、、、なにを言ってるか分からないですよね。ワタシも良く分かっていません。誰か分かりやすく教えて下さい。
PWAアプリをインストールする際に登録されたServiceWorkerを削除したうえで、キャッシュをクリアして画面をリロードするわけです。
そうしてあげることで、iPhone(iOS)のSafariやPWAアプリであってもアプリが更新されることを確認できました。

```src/App.vue
  :
<script>
  :
import { API, graphqlOperation } from 'aws-amplify';
import { listSampleAppsyncTables } from "../graphql/queries";
  :
      let apiResult = await API.graphql(graphqlOperation(listSampleAppsyncTables, { group : "version" }));
      let item = apiResult.data.listSampleAppsyncTables.items[0];
      let version = item.path;
      var versionCookies = Cookies.get('sub.w2or3w.work.version');
      Cookies.set('sub.w2or3w.work.version', version, { expires: 10 });
      if(version != versionCookies){
        window.navigator.serviceWorker.getRegistrations()
        .then(registrations => {
          for(let registration of registrations) {
            registration.unregister();
          }
        });
        window.location.reload(true);
      }
  :
```
registerServiceWorker.jsのregisteredをなんかゴニョゴニョするというような情報も見かけて試してみましたが、それによる効果がイマイチはっきり分からなかったのでここでは載せないようにします。
registerServiceWorker.jsは特にほぼほぼ理解できていないので、何でこれで良いのかはっきりとした説明をすることはできませんが、今日のところはこれで勘弁してください。

## あとがき
聞きかじった情報なのですが、PWAってストアに登録することもできるらしいですね。
Google Play、iOS App Store、Windows Store。
いつかチャレンジしてみたいものです。
あとは、プッシュ通知もサポートできるようになりたいので、まだまだPWAについて学ぶことは多そうです。

しかしiPhone(iOS)はPWAまわり色々と問題がありそうですね。
先日JAWS-UG浜松のAmplifyおじさん(年下)から、iPhoneのPWAアプリでファイルチューザーを開いたままホームを表示すると、その後二度とファイルチューザーを開けなくなってしまう、という報告をもらいました。
二度と、と書きましたが、復活させるための手順はあります。バージョンによって違うみたいですが、Killプロセスすれば復活したり、PWAアプリを再インストールしないと復活しなかったり、といったところです。端末再起動でも復活する予感がしますが面倒なので試してません。

iPhone(iOS)特有の自称はPWA以外にもいくつか食らっているので、そのうち記事にまとめたいなと思ってます。
かつてのアイイー対応のように、アイフォン対応みたいな世界にならないことを祈ります。