gatsby-plugin-offlineでWorkbox(runtimeCaching)の設定を上書きする際の注意点

8 min read読了の目安(約7700字

gatsby-plugin-offlineというプラグインがあります。

https://www.gatsbyjs.com/plugins/gatsby-plugin-offline/

Workbox Build を使用してサイトのServiceWorkerを作成し、サイトアクセス時にクライアント側にService workerをロードさせ、ServiceWorkerを用いた機能を使えるようにするためのプラグインです。
このプラグインでは、workboxConfigというオプションを利用することで、デフォルトのWorkbox configを上書きすることができます。

Overriding Workbox configuration - gatsby-plugin-offline

デフォルトのWorkbox configは下記のような内容となっています。
このソースコードは上のリンク先にあるものをそのまま引用しています。

const options = {
  importWorkboxFrom: `local`,
  globDirectory: rootDir,
  globPatterns,
  modifyURLPrefix: {
    // If `pathPrefix` is configured by user, we should replace
    // the default prefix with `pathPrefix`.
    "/": `${pathPrefix}/`,
  },
  cacheId: `gatsby-plugin-offline`,
  // Don't cache-bust JS or CSS files, and anything in the static directory,
  // since these files have unique URLs and their contents will never change
  dontCacheBustURLsMatching: /(\.js$|\.css$|static\/)/,
  runtimeCaching: [
    {
      // Use cacheFirst since these don't need to be revalidated (same RegExp
      // and same reason as above)
      urlPattern: /(\.js$|\.css$|static\/)/,
      handler: `CacheFirst`,
    },
    {
      // page-data.json files, static query results and app-data.json
      // are not content hashed
      urlPattern: /^https?:.*\/page-data\/.*\.json/,
      handler: `StaleWhileRevalidate`,
    },
    {
      // Add runtime caching of various other page resources
      urlPattern: /^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
      handler: `StaleWhileRevalidate`,
    },
    {
      // Google Fonts CSS (doesn't end in .css so we need to specify it)
      urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
      handler: `StaleWhileRevalidate`,
    },
  ],
  skipWaiting: true,
  clientsClaim: true,
}

デフォルトの挙動

gatsby-plugin-offlineを利用する場合、gatsby-plugin-offline自体をインストールしたあとで、gatsby-config.jsに下記のような記述を追加します。

module.exports = {
  ・
  ・
  ・
  plugins: [
    ・
    ・
    ・
   `gatsby-plugin-offline`,
  ],
}

そしてプロジェクトのビルドを行うと、何も設定を行わなければ public/sw.jsの中にgatsby-plugin-offlineのデフォルト設定が吐き出されます。
(先ほど貼ったデフォルトのWorkbox configの内容ですね)

実際に吐き出されたソースコードをここに貼ると、スクロールが大変になるので割愛しますが、例えばruntimeCachingについては下記のような記述が吐き出されます。

workbox.routing.registerRoute(/(\.js$|\.css$|static\/)/, new workbox.strategies.CacheFirst(), 'GET');
workbox.routing.registerRoute(/^https?:.*\/page-data\/.*\.json/, new workbox.strategies.StaleWhileRevalidate(), 'GET');
workbox.routing.registerRoute(/^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/, new workbox.strategies.StaleWhileRevalidate(), 'GET');
workbox.routing.registerRoute(/^https?:\/\/fonts\.googleapis\.com\/css/, new workbox.strategies.StaleWhileRevalidate(), 'GET');

ここにはどのようなキャッシュ戦略を取るかが記載されています。
(そしてこのキャッシュ戦略に関することで少し躓いたので、その旨をこれから書いていきます)

runtimeCachingの設定を上書きする場合の、やりがち?な失敗例

gatsby-plugin-offlineを使っていて、私は上に書いたruntimeCachingの内容を上書きしたくなりました。

gatsby-plugin-offlineの場合、この設定を行うのはとても簡単です。
gatsby-config.jsに下記のように記述するだけです。
(後述しますが、この設定方法には誤りが含まれています)

module.exports = {
  ・
  ・
  ・
  plugins: [
    ・
    ・
    ・
   {
      resolve: `gatsby-plugin-offline`,
      options: {
        workboxConfig: {
          runtimeCaching: [
            {
              urlPattern: /^https?:.*\/page-data\/.*\.json/,
              handler: 'NetworkFirst',
            },
          ],
        },
      },
    },
  ],
}

/^https?:.*\/page-data\/.*\.json/というURLパターンのキャッシュ戦略をStaleWhileRevalidateからNetworkFirstに変更するためのコードです。
(繰り返しますが、これは誤っています)

StaleWhileRevalidateNetworkFirstに関する解説はそれなりの字数とともに脇道にそれてしまうので、割愛します。
気になる方は下記のドキュメントをご覧ください。
図付きで大変わかりやすい説明となっています。

https://developers.google.com/web/tools/workbox/modules/workbox-strategies

runtimeCachingの設定を上書きする際のハマりどころ

さて、このruntimeCachingの設定を行った状態でgatsbyプロジェクトのビルドを行いました。
出力されるsw.jsについてですが、下記のような内容となります(runtimeCachingのみ抜粋)。

workbox.routing.registerRoute(/^https?:.*\/page-data\/.*\.json/, new workbox.strategies.NetworkFirst(), 'GET');
workbox.routing.registerRoute(/^https?:.*\/page-data\/.*\.json/, new workbox.strategies.StaleWhileRevalidate(), 'GET');
workbox.routing.registerRoute(/^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/, new workbox.strategies.StaleWhileRevalidate(), 'GET');
workbox.routing.registerRoute(/^https?:\/\/fonts\.googleapis\.com\/css/, new workbox.strategies.StaleWhileRevalidate(), 'GET');

私が想定していたのは/^https?:.*\/page-data\/.*\.json/のURLパターンに設定されていたStaleWhileRevalidateというキャッシュ戦略をNetworkFirstに上書きしたいだけでした。
ところがデフォルトで設定されていた/(\.js$|\.css$|static\/)/のURLパターンのCacheFirstが上書きされてしまっています。

なぜこのような挙動になるのでしょうか?

これは実装を見ればすぐに分かります。

https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-plugin-offline/src/gatsby-node.js#L162

上に貼った箇所でlodashのmergeが使われていますが、今回の挙動はこのmergeの挙動によるものです。

const combinedOptions = _.merge(options, workboxConfig)

下記のソースコードはlodashのmergeの挙動についてです。
JSBinにも同様のソースコードを載せています。

const merged = _.merge([
  {
    name: 'banana',
    price: 80
  }, {
    name: 'apple',
    price: 120
  }
], [
  {
    name: 'orange',
    price: 180
  }
]);

console.log(merged); 
/*
👇
[[object Object] {
  name: "orange",
  price: 180
}, [object Object] {
  name: "apple",
  price: 120
}]

このような形でlodashのmergeを用いた場合、配列の1番目の項目に対して置き換えが行われます。
(つまりruntimeCachingの1つ目の設定である /(\.js$|\.css$|static\/)/というURLパターンについて置き換えが行われました)

そのため、意図しない設定の上書きが発生したというわけです。

正しいruntimeCachingの設定の上書き例

上の設定を正しく行うためには下記のような設定を行うべきでした。

module.exports = {
  ・
  ・
  ・
  plugins: [
    ・
    ・
    ・
    {
      resolve: `gatsby-plugin-offline`,
      options: {
        workboxConfig: {
          runtimeCaching: [
            {
              urlPattern: /(\.js$|\.css$|static\/)/,
              handler: `CacheFirst`,
            },
            {
              urlPattern: /^https?:.*\/page-data\/.*\.json/,
              handler: `NetwoekFirst`,
            },
            {
              urlPattern: /^https?:.*\.(png|jpg|jpeg|webp|svg|gif|tiff|js|woff|woff2|json|css)$/,
              handler: `StaleWhileRevalidate`,
            },
            {
              urlPattern: /^https?:\/\/fonts\.googleapis\.com\/css/,
              handler: `StaleWhileRevalidate`,
            },
          ],
        },
      },
    },

勘違いした要因について

私は最初、同じURLパターンが存在していればそのURLの設定に対してreplaceを行い、存在しないURLが設定されているのならappendされる挙動だと勘違いしていました。

私の英語読解能力+ソフトウェアエンジニア的教養が足りていなかったことが原因ではあるかと思いますが、私が感じたことに近いissueも見つけました。
(ちなみにこのissueは、この文章を書いている間に時間切れでcloseしてしまったようです)
gatsby-plugin-offline: Allowed options and runtimeCaching option merge behaviour

GatsbyのHow to Contributeをページを読み進めていけば、すぐにどういう手順でコントリビューションに移れるかは明確でしたので、何らかのPRを作成して貢献できたらと考えたのですが、設定の上書きに関する挙動を変えるのはよろしくないので、現在の挙動を維持したままどのように対応していこうかと考えています。

いま、こうやって文章を書いている間に、上のissueで書かれていた提案のようなものにしたほうが良いのかもと考えはじめましたが、皆さんはどう思われるでしょうか?
(runtimeCachingMergeStrategy というoptionを用いて、merge戦略を明示的に指定する方法。このオプションを取らない場合は、既存の挙動を維持する)

最後に

というわけで、gatsby-plugin-offlineの絶妙なハマリポイントについて書いてきました。

割とキャッシュの設定は誤ると、後々地味に面倒な事になりがちなので、gatsby-plugin-offlineで設定を行う際はご注意ください。
(gatsby-plugin-offline自体、ソースコードの規模は小さいので、一度中をざっくり読んでから触るのでも良いかもしれません)

また、gatsby-plugin-offlineの設定方法について、グッドな提案をお持ちの方はGatsbyにPR飛ばしてみると良いかもしれません。
(こんなことを書いていますが、私は別にGatsbyの中の人とかではありません。)

私も時間見つけてちょっとアイデア考えてみようと思います。
(時間見つけてやる的なセリフは、大抵かなり後回しになるものですが...)