SPAで起こるリビジョンのズレ問題とその解決方法

この記事は個人ブログと同じ内容です


はじめに

現在、Nuxt.jsを使ってSPA(Single Page Application)としてフロントエンドを構築しています。その中で、ユーザーが長時間アプリを開いたままにすることで発生する「リビジョンの不整合」問題に直面しました。この記事では、その問題の背景と、実際に導入した対策について紹介します。


🐛 なぜリビジョンの不整合が起こるのか?

SPA(Single Page Application)では、初回アクセス時に1つのHTMLと、JavaScriptCSSなどの静的ファイルが読み込まれます。その後はページ遷移しても画面全体が再読み込みされることはなく、クライアント側でJavaScriptが動き続ける仕組みになっています。

この特性により、ユーザーがアプリを開いたまま何時間も操作し続けている場合でも、ページをリロードしない限りJavaScriptは更新されません

一方、アプリケーションの開発者側は定期的に新しいバージョンをデプロイし、コードやAPIの仕様を変更していきます。すると次のようなズレが発生します:

  • ユーザー側: 数時間前に読み込んだ古いJavaScriptを実行中
  • サーバー側: 最新バージョンのAPIや仕様に切り替わっている

これにより、古いJavaScriptが新しいAPIにアクセスしようとして不整合が発生したり、エラーや意図しない挙動につながることがあります。

特に以下のようなケースでは深刻な問題となります:

  • バリデーション仕様が変更されサーバーとの通信でエラーが発生する
  • フロント側の状態管理とサーバーのデータ構造に不整合が生じる
  • 削除された機能を呼び出そうとしてクラッシュする

このような問題は、ユーザー自身が「一度リロードしてください」と言われない限り気づけないため、プロダクション環境では放置すると深刻な不具合に発展します


🔧 対策方法

SPAでのリビジョンの不整合による不具合を防ぐために、アプリケーションの現在のバージョン(リビジョン)を管理し、サーバー側とクライアント側で定期的に照合する仕組みを取り入れます。

具体的な対策の流れ:

  1. ビルド時にユニークなリビジョンIDを生成

    • 日付とGitのコミットハッシュなどを組み合わせて一意のIDを作成。
  2. 生成したリビジョンIDをversion.jsonファイルとして出力し、サーバーに配置

    • このファイルをクライアント側が定期的に参照する。
  3. クライアント側でリビジョンIDを定期的にチェック

    • ページ遷移時、一定間隔ごと(例:1分)、またはタブが再アクティブ化された時にサーバーのversion.jsonを取得。
  4. クライアントとサーバーのリビジョンが不一致の場合、ページを強制リロード

    • 古いバージョンを保持したまま操作を続けることによる不具合を防ぐ。

この仕組みにより、ユーザーが意識せずとも最新のバージョンに追従できる環境が整い、プロダクション環境でのトラブル防止につながります。


⚙️ 実装

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開発では必須の仕組みと言えるでしょう。

ぜひあなたのプロジェクトでも導入してみてください。