Vue.jsをPWA化してService WorkerにRuntime Cacheを実装、そして公開するまで

# はじめに
先日iPhoneを買ったので何か面白いことできないかなと思い、PWAを作りました。
本記事はVue.jsをPWA化し、Service WorkerにRuntime Cacheを実装してそれを公開する手順について紹介します。（本当はWeb Pushなんかもやってみたかったですが、2019/09現在、Safariは未対応のためできず。。）
Vue.jsやTypeScriptの書き方は解説しません。

# 今回の成果物
今回はQiitaのAPI(指定したタグが付けれられた投稿一覧を返すAPI)の結果をService Workerにキャッシュさせ、一度検索したものはオフラインでも取得できるようなPWAを作成しました。
以下はそのデモ動画になります。


<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Runtime Cacheを実装したPWAのサンプル <a href="https://t.co/0GQEhCbU1l">pic.twitter.com/0GQEhCbU1l</a></p>&mdash; Ryutaro Kawaguchi (@kawakawa_dev) <a href="https://twitter.com/kawakawa_dev/status/1171106792371130376?ref_src=twsrc%5Etfw">2019年9月9日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


## 動作の解説
起動後ネットワークが接続された状態で、まず「java」というタグで検索します。
するとjavaというタグの投稿一覧が表示されます。
その後iPhoneのネットワークを切断します。（ここでオフライン状態）
切断後「ruby」というタグで検索するとオフラインのため結果を取得できず表示されないことが分かります。
しかし、次に再度「java」というタグで検索するとオフラインにも関わらず投稿一覧が表示できていることが分かります。

## アプリケーションとソースコード
アプリケーションはGithub Pagesに公開しています。
https://kawakawaryuryu.github.io/vue-sample-pwa/

ソースコードは以下になります。
https://github.com/kawakawaryuryu/vue-sample-pwa

## 利用時の注意
QiitaのAPIは認証なしで使用しています。そのため、1時間に60回以上たたくと利用制限に引っかかり使用できなくなりますので、検索のしすぎにはご注意ください。

[Qiita API v2ドキュメント - Qiita:Developer](https://qiita.com/api/v2/docs)
> 利用制限
> 認証している状態ではユーザごとに1時間に1000回まで、認証していない状態ではIPアドレスごとに1時間に60回までリクエストを受け付けます。


# 前提
使用したツールやライブラリとそのバージョンは以下になります。

|ツール・ライブラリ|バージョン|
|:-:|:-:|
|Vue.js|2.6.10|
|TypeScript|3.4.3|
|@vue/cli-plugin-pwa|3.10.0|
|axios|0.19.0|

また、あらかじめVue.jsのプロジェクトが作成されており、TypeScriptがインストールされている前提で進めます。

# 事前知識
必要となる知識は「[PWAとは何かまとめる - Qiita](https://qiita.com/kawakawaryuryu/items/0db73e5b6c43638d6169)」にまとめてありますのでご覧ください。

# Vue.jsでのPWA化手順
ここからは実際にVue.jsをPWA化する手順を紹介します。
正直あっという間です。

## 1. Vue CLIのPWAプラグインの追加
Vue.jsにはPWAプラグインがあらかじめ用意されています。
これを使うとPWAに必要なファイルやライブラリをインストールすることができます。
今回はこれを使ってPWA実装を行っていきます。

```
$ yarn add @vue/cli-plugin-pwa --dev
```

## 2. PWAプラグインを使って必要なソースや設定一式をインストール
以下コマンド実行でPWAに必要なファイルやライブラリを追加します。

```
$ vue add pwa
```

実行すると以下のファイルたちが追加or更新されます。

```
     public/img/icons/android-chrome-192x192.png
     public/img/icons/android-chrome-512x512.png
     public/img/icons/apple-touch-icon-120x120.png
     public/img/icons/apple-touch-icon-152x152.png
     public/img/icons/apple-touch-icon-180x180.png
     public/img/icons/apple-touch-icon-60x60.png
     public/img/icons/apple-touch-icon-76x76.png
     public/img/icons/apple-touch-icon.png
     public/img/icons/favicon-16x16.png
     public/img/icons/favicon-32x32.png
     public/img/icons/msapplication-icon-144x144.png
     public/img/icons/mstile-150x150.png
     public/img/icons/safari-pinned-tab.svg
     public/manifest.json
     public/robots.txt
     src/registerServiceWorker.ts
     package.json
     src/main.ts
     yarn.lock
```

|ファイル|内容|
|:-:|:-:|
|`public/img/icons/*.png`|端末に合わせたアイコン画像|
|`public/manifest.json`|Web App Manifest<br>PWAとして端末にインストール可能にするために必要|
|`src/registerServiceWorker.ts`|Service Workerの登録を担うファイル<br>インストールすると`main.ts`に勝手にimportしてくれるので、起動時にService Workerが自動で登録される|

参考

- [vue-cli/packages/@vue/cli-plugin-pwa at dev · vuejs/vue-cli](https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa)

ここまででPWAの下準備は整いました。（早い）
これだけでもPWAとして動かすことは可能ですが、ここからはService WorkerにAPIレスポンス結果をキャッシュさせるような実装を追加してPWAをより進化させていきます。

# Runtime Cacheの実装
次に、Service WorkerにQiitaのAPIレスポンス結果をキャッシュするようなRuntime Cacheを実装していきます。

## Workbox
Vue.jsのPWAプラグインを利用するとService Workerをゴリゴリ実装せずとも設定だけで簡単にPWAの色々な機能を実現することができます。
Runtime Cacheも設定だけで実現可能です。
それを実現してくれるのが[Workbox](https://developers.google.com/web/tools/workbox/)というライブラリです。
WorkboxはGoogleが提供するService Workerの実装ライブラリで、ボイラープレートになりがちな実装コードを提供してくれます。
そのため、開発者がJavaScriptを書かなくとも設定だけでService Workerの機能を利用することが可能です。
Vue.jsのPWAプラグインは内部でWorkboxを使用しています。
次から具体的なRuntime Cacheの実装までの手順を見ていきますが、内部にはこのWorkboxがいると思ってください。

## 1. APIコールを実装する
まず、QiitaのAPIを叩く部分の実装です。
HTTPクライアントとして、[axios](https://github.com/axios/axios)をインストールします。

```
$ yarn add axios
```

以下の実装はaxiosを用いてQiitaのAPIを叩き、指定したタグが付けられた記事一覧を取得し表示するコンポーネントになります。

```vue:QiitaSearch.vue
<template>
  <div>
    <input type="text" v-model="tag" />
    <button @click="search(tag)">search</button>
    <ul>
      <li v-for="article of articles" v-bind:key="article.id">
        {{ article.url }}
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { QiitaArticle } from '@/interface/QiitaArticle'
import axios, { AxiosResponse } from 'axios'

@Component
export default class QiitaSearch extends Vue {
  private articles: QiitaArticle[] = []
  private tag: string = ''

  public created() {
    this.articles = []
  }

  public async search(tag: string) {
    try {
      const response: AxiosResponse = await axios.get<QiitaArticle[]>(
        `https://qiita.com/api/v2/tags/${tag}/items`
      )
      this.articles = response.data
    } catch (e) {
      this.articles = []
    }
  }
}
</script>
```

## 2. vue.config.jsの設定
`vue.config.js`に以下のような設定を書くだけでRuntime Cacheの実装は終了です。
設定ファイルは`vue.config.js`ですが、設定項目はWorkboxのものと同じになります。
詳しい設定項目については[Workbox webpack Plugins &nbsp;|&nbsp; Workbox &nbsp;|&nbsp; Google Developers](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin)を参照してください。

```js:vue.config.js
module.exports = {
  ...
  pwa: {
    workboxOptions: {
      runtimeCaching: [
        {
          urlPattern: /https:\/\/qiita.com\/api\/.*/,
          handler: 'networkFirst',
          options: {
            cacheName: 'qiita-cache',
            expiration: {
              maxEntries: 10,
              maxAgeSeconds: 300
            },
            cacheableResponse: {
              statuses: [0, 200]
            }
          }
        }
      ]
    }
  },
  ...
}

```

# GitHub Pagesに公開する
最後に今回作成したPWAサンプルを公開する手順です。
冒頭でも述べた通り今回作成したPWAサンプルをGitHub Pagesに公開しました。

- https://kawakawaryuryu.github.io/vue-sample-pwa/

続いてはその手順を紹介。

## 1. vue.config.jsの修正

GitHub Pagesに公開するため、ビルドした際のindex.htmlの出力先を`docs`という名前のディレクトリにします(デフォルトは`dist`)。
また、GitHub Pagesはリポジトリ名がサブパスとして付加されるので、ルートパスを変更します。

```javascript:vue.config.js
module.exports = {
  ...
  outputDir: 'docs',
  publicPath: '/vue-sample-pwa/',
  ...
}
```

参考

- [Configuration Reference | Vue CLI](https://cli.vuejs.org/config/#publicpath)
- [Configuring a publishing source for GitHub Pages - GitHub Help](https://help.github.com/en/articles/configuring-a-publishing-source-for-github-pages)

## 2. eslintからdocsを外す
docsディレクトリはeslintの対象外としたいので以下ファイルを追加。

```:.eslintignore
docs/**
```

参考

- [eslintで任意のディレクトリ、ファイルをチェック対象から除外する方法 - Qiita](https://qiita.com/seito2014/items/bec113dcb461d704f0cb)

## 3. ビルド
デプロイするコードを生成します。
これにより`docs`ディレクトリが作成されます。

```
$ yarn build
```

## 4. GitHub上でGitHub Pagesとして公開するように設定する
Repository名 > Settings > Optionsを選択し、GitHub Pagesの設定箇所で以下ように「None」→「master branch /docs folder」を選択します。

<img width="708" alt="スクリーンショット 2019-09-10 01.14.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/89007/40bb79cb-4eef-3e61-7266-7e7c0fde49ff.png">

参考

- [GitHub Pages の公開ソースを設定する - GitHub ヘルプ](https://help.github.com/ja/articles/configuring-a-publishing-source-for-github-pages)

## 5. git push
実際にGitHub Pagesに公開します。

```
$ git push origin master
```

これでPWAがGitHub Pagesに公開されるようになりました！

以上