これはなに?
SPAのキャッシュコントロールは難しい問題だけど、ちゃんと仕組みを理解してただしい戦略を使えばそんなに難しくないよという話。
Disclaimer
この記事は2019年10月くらいにおける筆者の経験と見解に基づくものです。だいたい間違ってないとは思いますが内容の正確性は保証しませんし、2ヶ月くらいするとFirebaseないしNuxt.jsの進歩によりココに書かれていることが嘘になる可能性はあります。また、特定の記事や個人を中傷する意図はありません。筆者の所属する組織やその類とは関係がありません。
なんで難しい(と思われている)の?
そもそもの前提としてちゃんとやらないとアプリケーションのリリースがままならないというプレッシャーがあります。
そのうえで...
(1) SPAの構成ファイルをキャッシュしようとする輩がたくさんいる
- Hostingサービス(ブラウザキャッシュ)
- CDN
- (使っている場合は)サービスワーカー
(2) SPAを構成するファイルはどれをキャッシュしてどれをキャッシュしたくないかややこしい
- index.html
- JavaScriptたち
- CSS
- サービスワーカー
- その他の静的ファイル(画像とか)
といったあたりに原因があるんじゃないかと思ってます。
どうしたらいいの?
そもそも、SPAというのはindex.htmlを起点として他の諸々のファイルを読み込みに行くものです。
Nuxt.jsでアプリケーションをビルドすると、index.htmlから読み込まれるJavaScriptたちは都度ファイル名が変わります。
したがって、index.htmlさえキャッシュに乗らなければデプロイ直後でもちゃんと更新されるということになります。
キャッシュする/しない戦略
以下のようになります。
- index.html → キャッシュしない
- JavaScriptたち → キャッシュする
- CSS → キャッシュする(Nuxt.jsでは生成されない)
- サービスワーカー → 絶対にキャッシュしてはいけない
- その他の静的ファイル(画像とか)→ キャッシュしてよい(要件次第)
誰がキャッシュする?
サービスワーカーに任せます。
理由は次のとおりです。
- Firebase Hosting → 設定ごちゃごちゃさせるの嫌&オフライン対応もできなくなる
- サービスワーカー → そもそもそのために生まれてきた(言い過ぎ)
つまり、
- Firebase HostingではホストされるファイルをキャッシュさせないようにHTTPヘッダを設定する(特に何もしなくて良い)
- サービスワーカーでキャッシュをコントロール
ということになります。
でもサービスワーカーって難しいんじゃないの?
かんたんです。
Nuxt.jsではPWAモジュールがいるのでこいつを導入してください。
https://pwa.nuxtjs.org
インストールして
$ npm install @nuxtjs/pwa
modulesに登録
export default {
modules: [
'@inkline/inkline/nuxt',
'@nuxtjs/markdownit',
'@nuxtjs/pwa'
],
//...
}
おわりです。
(staticディレクトリがない場合は作っておいてください)
そうするとどうなる?
ビルド時にsw.jsという名前のファイルがstaticディレクトリに自動生成されます。そして、アプリケーションではこいつを利用するようになります。
だいたい以下のような内容のはずです。
importScripts('https://cdn.jsdelivr.net/npm/workbox-cdn@4.3.1/workbox/workbox-sw.js')
// --------------------------------------------------
// Configure
// --------------------------------------------------
// Set workbox config
workbox.setConfig({
"debug": false
})
// Start controlling any existing clients as soon as it activates
workbox.core.clientsClaim()
// Skip over the SW waiting lifecycle stage
workbox.core.skipWaiting()
workbox.precaching.cleanupOutdatedCaches()
// --------------------------------------------------
// Precaches
// --------------------------------------------------
// Precache assets
// --------------------------------------------------
// Runtime Caching
// --------------------------------------------------
// Register route handlers for runtimeCaching
workbox.routing.registerRoute(new RegExp('/_nuxt/'), new workbox.strategies.CacheFirst ({}), 'GET')
workbox.routing.registerRoute(new RegExp('/'), new workbox.strategies.NetworkFirst ({}), 'GET')
最後の2行がキャッシュの設定です。
Workboxのキャッシュ戦略
Nuxt.jsのPWAモジュールはWorkboxをベースにしています。このWorkboxは3種類のキャッシュ戦略があります。
- NetworkFirst: ネットワーク優先でコンテンツを返します。(キャッシュはネットワークが利用できないときに限り利用します)
- CacheFirst: キャッシュ優先でコンテンツを返します。(キャッシュがあればネットワークを無視します)
- StaleWhileRevalidate: キャッシュがあればキャッシュからコンテンツを返しつつ、裏でキャッシュ更新を行います。古くても直ちには死なないが速度は重要なファイルに対して使います。
で、どうすればよかったんだっけ?
今回の例では index.htmlにNetworkFirst、その他のファイルにCacheFirstを採用すればよさそうです。
これを踏まえて、自動生成されたサービスワーカーを見てみましょう。
// (省略)
workbox.routing.registerRoute(new RegExp('/_nuxt/'), new workbox.strategies.CacheFirst ({}), 'GET')
workbox.routing.registerRoute(new RegExp('/'), new workbox.strategies.NetworkFirst ({}), 'GET')
/_nuxt/
はビルドされたJavaScriptファイルが吐かれるパス(の既定値)ですね。CacheFirstとなっています。
/
はindex.htmlが置かれるルートです。こちらは確かにNetworkFirstです。
単純なリビジョンアップを実現するなら何も考える必要はなさそうです。便利ですね。
まとめ
リビジョンアップはPWAモジュールを使ってくれ頼む。
ページ遷移時に強制アップデートかけたいんだけど?
この方式ではアップデートのタイミングがユーザがリロード(に相当する処理)を行ったときになります。
個人的な意見としては、middlewareをつかってページ遷移間で頑張って強制アップデートをかけるようなことはしないほうがいいと思います。
突然リロードが発生するのはUX的にちょっと・・・とか、SSRとの相性とか、そもそもそこで都度ネットワークアクセスさせたいか(失敗したらどうすんの)とか、ファイルキャッシュがうまく制御できなくて苦しんでるのにjsonファイル置いて解決するような手法はどうなのとか、理由は有限にあります。
それでもやりたいというのであれば、1000000歩譲ってmiddlewareを使うのは仕方ないとして、
バージョンを管理するjsonファイルを置くような真似はせず、現代を生きる我々はRemote Configを使いましょう。
Remote ConfigはWebアプリでも使えるようになりました!!!!1!
https://firebase.google.com/docs/remote-config
ネイティブアプリ界では有名なソリューションでお世話になっている方も多いと思います。
Firebaseをせっかく使うならRemote Configにしましょう。そのための機能ですよ。