【Nuxt.js】pwa-moduleのちょっとしたTIPS

この記事は[パーソルキャリア Advent Calendar 2018](https://qiita.com/advent-calendar/2018/pca)の3日目です。

昨日に続けてNuxtとPWAの記事となります。昨日は主に`preCache`についてでしたが、今日は`runtimeCache`のお話です。

## この記事の内容
- runtimeCacheとは
- pwa-moduleでruntimeCacheを設定する
- runtimeCacheの意図しない挙動とその原因
- 意図通りにruntimeCacheされるようにする

## この記事で触れないこと
- Nuxt.jsとは何か
- PWAとは何か

Nuxtについては公式ドキュメントがわかりやすいので[こちら](https://ja.nuxtjs.org/)をご参照ください。
PWAについては、[Googleのコードラボ](https://codelabs.developers.google.com/codelabs/your-first-pwapp-ja/#0)や[私自身が簡単にまとめたスライド](https://speakerdeck.com/gyarasu)をご参照いただければと思います。

## tl;dr
- 　pwa-moduleを使う場合、同一オリジンのパスをruntimeCacheに指定するとデフォルト設定の影響で意図どおりに動作しない
- 　`offline: false`を設定し、デフォルト設定を自前で追加して、URLパターンマッチの順番をコントロールするとうまくいく

## runtimeCacheとは
Service Workerは大きく分けて2種類のキャッシュを扱うことができます。簡単に説明すると次のようになります。

- preCache: AppShellに該当する静的アセットのキャッシュ
- runtimeCache: 非同期で呼び出すAPIのレスポンスなど、動的なデータのキャッシュ

runtimeCacheは取得するリソースの特性によって、キャッシュ戦略をうまく設定してあげると速度と更新性を最適化するのに有効です。キャッシュ戦略については、[Workboxのページ](https://developers.google.com/web/tools/workbox/modules/workbox-strategies)が図解があってわかりやすいです。


## pwa-moduleでruntimeCacheを設定する
Nuxtプロジェクトにpwa-moduleを導入したら、`nuxt.config.js`でruntimeCacheの設定を書くことができます。

```javascript:nuxt.config.js
   workbox: {
     skipWaiting: true,
     clientsClaim: true,
     runtimeCaching: [
       {
         // APIから取得した結果
         urlPattern: '/api/xxxx/.*',
         handler: 'cacheFirst',
         method: 'GET',
         strategyOptions: {
           cacheExpiration: {
             maxAgeSeconds: 60 * 60 * 24, // 1日
           },
           cacheableResponse: {
             statuses: [200],
           },
         },
       },
     ],
   }
```

試しにこのように設定してみます。
例えばサービスが`https://your.domain`で動いていたとすると、`https://your.domain/api/xxxx/.*`に合致するリクエストはキャッシュを最初に見に行くという設定です。もしキャッシュが存在しなければ（もしくはキャッシュへのアクセスに失敗したら）ネットワークから取得します。
なので、キャッシュからリソースが取得できればかなり早いレスポンスタイムになります。

`skipWaiting`と`clientsClaim`については今回は詳しく言及しませんが、簡単にいうと「初回の読み込みからService Workerがページをコントロールできるようにする設定」です。


## runtimeCacheの意図しない挙動とその原因
### 意図通りに動かない事象
意気揚々とruntimeCacheを設定して、実行してみました。
が!**「キャッシュの割になんか遅いぞ...？」**となりました。

Chromeのデベロッパーツールを見るとService Workerからレスポンス来てるし、実際にキャッシュも存在するのんいなぜ...？
挙動を見る限り、networkFirstで動いている感じでした。cacheFirstがpwa-moduleでサポートされてないのかと一瞬思いましたが内部的にworkboxが使われているしそんなはずはありません。

### 原因
ビルド時に生成される`sw.js`を見てみたところ、原因が判明しました。
下記は`sw.js`のruntimeCacheの設定が出力されている部分を抜粋したものです。

```javascript:sw.js
 workbox.routing.registerRoute(new RegExp('/.*'), workbox.strategies.networkFirst({}), 'GET')

 workbox.routing.registerRoute(new RegExp('/api/xxxx/.*'), workbox.strategies.cacheFirst({"cacheExpiration":{"maxAgeSeconds":900},"cacheableResponse":{"statuses":[200]}}), 'GET')
```

ん？なんか身に覚えのないやつがいるぞ...?

```javascript
workbox.routing.registerRoute(new RegExp('/.*'), workbox.strategies.networkFirst({}), 'GET')
```

試しに`nuxt.config.js`の`runtimeCache`の内容を削除してビルドしてみましたが、デフォルトで入る設定のようです。

### URLパターンがマッチするのに動作しない
ここでのポイントは、

- 自分で設定に書いたURLパターン'/api/xxxx/.*'が、`sw.js`に存在するruntimeCacheの両方のURLパターンにマッチする
- 実際の挙動は先に書かれているものに従う

ということです。

デフォルトで出力される設定は、決して邪魔しようとしているのではなく、同一オリジンのすべてに対してcacheを使えるようにして、オフライン（ネットワークからリソースが取得できない）の場合にも、キャッシュにあるデータを代用することでアプリケーションが動作するようにすることを意図した、むしろ親切な挙動といえます。

URLパターンは先にマッチしたほうが採用される動きになるので、**自分で書いた設定をデフォルトで出力される設定より前に埋め込められれば幸せになれそう**です。
実際に、`sw.js`を直接書き換えて動かしてみると、意図通りにcacheFirstになり、レスポンスも速くなりました。


## 意図通りにruntimeCacheされるようにする
Workboxモジュールも[ドキュメント](https://pwa.nuxtjs.org/modules/workbox)がありますが、今回の範囲は網羅されてないようでした。ということでソースコードを見てみます。

https://github.com/nuxt-community/pwa-module/blob/dev/packages/workbox/index.js#L115-L121

```javascript:workbox/index.js
   // Optionally cache other routes for offline
   if (options.offline && !options.offlinePage) {
     options._runtimeCaching.push({
       urlPattern: fixUrl(`${routerBase}/.*`),
       handler: 'networkFirst'
     })
   }
```

これっぽいです。
どうやら`offline`に`false`を設定してあげればいけそうです。このオプションはデフォルトで`ture`になっていました。
そうすると、デフォルトの親切設定が出力されなくなるので必要な場合は自分でしていしてあげる必要があります。

```javascript:nuxt.config.js
    workbox: {
      offline: false,
      skipWaiting: true,
      clientsClaim: true,
      runtimeCaching: [
        {
          // 求人検索APIから取得した結果
          urlPattern: `/api/xxxx/.*`,
          handler: 'cacheFirst',
          method: 'GET',
          strategyOptions: {
            cacheExpiration: {
              maxAgeSeconds: 60 * 60 * 24, // 1日
            },
            cacheableResponse: {
              statuses: [200],
            },
          },
        },
        {
          // デフォルト（最後に記述する）
          urlPattern: '/*',
          handler: 'networkFirst',
          method: 'GET',
        },
      ],
    }
```

`/*`に対してnetworkFirstを設定してあげます。ここで重要なのは、**最後に入れてあげる**ことです。
先に述べたように、`sw.js`内で上からURLパターンをマッチさせていき、最初にマッチした戦略がてきようされることになります。

この状態でビルドするとこんな感じになります。

```javascript:sw.js
// 自分でコントロールしたいもの
 workbox.routing.registerRoute(new RegExp('/api/xxxx/.*'), workbox.strategies.cacheFirst({"cacheExpiration":{"maxAgeSeconds":900},"cacheableResponse":{"statuses":[200]}}), 'GET')

// デフォルトで設定されるのを明示的に指定したもの
 workbox.routing.registerRoute(new RegExp('/.*'), workbox.strategies.networkFirst({}), 'GET')
```

めでたしめでたし！

## デフォルトの挙動の変更
https://github.com/nuxt-community/pwa-module/blob/dev/packages/workbox/index.js#L150

```javascript:workbox/index.js
       runtimeCaching: [].concat(options._runtimeCaching, options.runtimeCaching).map(i => (Object.assign({}, i, {
         urlPattern: i.urlPattern,
         handler: i.handler || 'networkFirst',
         method: i.method || 'GET'
       })))
```
見たところ、デフォルトの設定が自分で設定したものよりも上に出力される要因はこの処理みたいです。
`options._runtimeCaching`と`options.runtimeCaching`の順番を入れ替えてあげれば、今回の記事で書いたような対応はしなくて良さそうです。

ということで、[プルリクエスト](https://github.com/nuxt-community/pwa-module/pull/106)を出しました。
良さそうであればイイカンジのリアクションをよろしくお願いいたしますmm


## まとめ
pwa-moduleはとても便利ですが、たまにかゆいところに手が届かないことがあります。
かゆいところをかけるようにするために、OSSに貢献するというのも良いなと思いました。

Nuxt×PWA盛り上げていきたい！

## 追記（2019/02/08）
上記の変更を含むβ版がリリースされています。
https://github.com/nuxt-community/pwa-module/releases