この記事は個人ブログと同じ内容です
はじめに
現在、Nuxt.jsを使ってSPA(Single Page Application)としてフロントエンドを構築しています。その中で、ユーザーが長時間アプリを開いたままにすることで発生する「リビジョンの不整合」問題に直面しました。この記事では、その問題の背景と、実際に導入した対策について紹介します。
🐛 なぜリビジョンの不整合が起こるのか?
SPA(Single Page Application)では、初回アクセス時に1つのHTMLと、JavaScriptやCSSなどの静的ファイルが読み込まれます。その後はページ遷移しても画面全体が再読み込みされることはなく、クライアント側でJavaScriptが動き続ける仕組みになっています。
この特性により、ユーザーがアプリを開いたまま何時間も操作し続けている場合でも、ページをリロードしない限りJavaScriptは更新されません。
一方、アプリケーションの開発者側は定期的に新しいバージョンをデプロイし、コードやAPIの仕様を変更していきます。すると次のようなズレが発生します:
- ユーザー側: 数時間前に読み込んだ古いJavaScriptを実行中
- サーバー側: 最新バージョンのAPIや仕様に切り替わっている
これにより、古いJavaScriptが新しいAPIにアクセスしようとして不整合が発生したり、エラーや意図しない挙動につながることがあります。
特に以下のようなケースでは深刻な問題となります:
- バリデーション仕様が変更されサーバーとの通信でエラーが発生する
- フロント側の状態管理とサーバーのデータ構造に不整合が生じる
- 削除された機能を呼び出そうとしてクラッシュする
このような問題は、ユーザー自身が「一度リロードしてください」と言われない限り気づけないため、プロダクション環境では放置すると深刻な不具合に発展します。
🔧 対策方法
SPAでのリビジョンの不整合による不具合を防ぐために、アプリケーションの現在のバージョン(リビジョン)を管理し、サーバー側とクライアント側で定期的に照合する仕組みを取り入れます。
具体的な対策の流れ:
ビルド時にユニークなリビジョンIDを生成:
- 日付とGitのコミットハッシュなどを組み合わせて一意のIDを作成。
生成したリビジョンIDをversion.jsonファイルとして出力し、サーバーに配置:
- このファイルをクライアント側が定期的に参照する。
クライアント側でリビジョンIDを定期的にチェック:
- ページ遷移時、一定間隔ごと(例:1分)、またはタブが再アクティブ化された時にサーバーのversion.jsonを取得。
クライアントとサーバーのリビジョンが不一致の場合、ページを強制リロード:
- 古いバージョンを保持したまま操作を続けることによる不具合を防ぐ。
この仕組みにより、ユーザーが意識せずとも最新のバージョンに追従できる環境が整い、プロダクション環境でのトラブル防止につながります。
⚙️ 実装
✅ GitHub Actionsを使用したリビジョンIDの作成
デプロイ時にGitHub ActionsでリビジョンIDを含んだJSONファイルを作成します。リビジョンIDは前のバージョンと重複しなければ問題ないので、わかりやすくするため、日付-Gitのハッシュにしています。Nuxt.jsを使用しているので、作成したファイルは./staticに配置しています。
Nuxt.jsのファイル構成については以下の通りなので注意が必要です:
./: プロジェクトルート直下。ビルドに含まれない./static/: Nuxtの公開ディレクトリ(ビルド後dist/version.jsonになる)=Webでアクセス可能./dist/: ビルド後の成果物。直接編集は推奨されない
- name: Generate version.json
shell: bash
run: |
REVISION=$(git rev-parse --short HEAD)
DATE=$(TZ='Asia/Tokyo' date +'%Y%m%d-%H%M%S')
echo "{\"revision\": \"${DATE}-${REVISION}\"}" > ./static/version.json
echo "🔗 App Revision: ${DATE}-${REVISION}"
✅ リビジョンの比較を行う
すべての画面でリビジョンの比較をしたいので、Nuxt.jsのPluginで実装していきます。
チェックするタイミングは: - ページ遷移時 - タブがアクティブになった時 - 一定時間ごと(ポーリング)
以下のようにfetch()を使用することで、サーバーにあるversion.jsonを取得できます。この時にオプションとしてno-cacheを使用することで、キャッシュを無視してサーバーの最新情報を確認します。
const res = await fetch('/version.json', { cache: 'no-cache' })
const { revision } = await res.json()
✅ Nuxt Plugin 実装例
import type { NuxtAppOptions } from '@nuxt/types'
declare global {
interface Window {
__APP_REVISION__?: string
}
}
export default ({ app }: { app: NuxtAppOptions }) => {
const CHECK_INTERVAL = 60 * 1000 // 1分
let lastCheck = 0
async function checkRevision() {
const now = Date.now()
if (now - lastCheck < CHECK_INTERVAL) return
lastCheck = now
try {
const res = await fetch('/version.json', { cache: 'no-cache' })
const { revision } = await res.json()
if (window.__APP_REVISION__ && revision !== window.__APP_REVISION__) {
console.log('[revision] update detected:', revision)
window.location.reload()
}
} catch (err) {
console.warn('[revision check failed]', err)
}
}
app.router?.afterEach(() => {
console.log('[revision] checking after route change')
checkRevision()
})
setInterval(() => {
console.log('[revision] checking by interval')
checkRevision()
}, CHECK_INTERVAL)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
console.log('[revision] checking on tab activation')
checkRevision()
}
})
}
まとめ
今回は、Nuxt.jsでSPAを開発する際に起こりがちなリビジョンの不整合問題と、その対策について解説しました。
- SPAはクライアント側で動き続けるため、古いJavaScriptを使い続けるとサーバーとの不整合が生じる
- ビルド時に一意のリビジョンIDを作成し、version.jsonで管理することで不整合を検知可能にする
- クライアント側で定期的にリビジョンチェックを行い、異なる場合は強制リロードして最新バージョンに切り替える
この対策により、ユーザーに負担をかけずに常に最新の状態を維持でき、プロダクション環境での不具合を未然に防げます。SPA開発では必須の仕組みと言えるでしょう。
ぜひあなたのプロジェクトでも導入してみてください。