4Hえんぴつ|React+TypeScriptでWeb RPGを作る
第8章「Web公開・互換性・永続化」
第42回:React RouterのSPAをNetlifyで直URL・再読み込み対応する
1. 開発環境では動いたのに、公開すると404になる
前回は、
React / Game Logic
↓
Save Service
↓
Save Repository
↓
Dexie.js
↓
IndexedDB
という形で、ゲームと永続化技術の境界を整理しました。
今回は、保存ではなくURLと画面遷移の話です。
このWeb RPGでは、React Routerを使って、
/
↓
タイトル
/character
↓
キャラクター設定
/saves
↓
セーブデータ管理
/town
↓
町
のように画面を分けています。
開発中は普通に動いていました。
ところが、SPAをそのまま公開すると、
トップページから遷移すれば動くのに、
/characterを直接開くと404になる
という問題が起こることがあります。
今回は、この問題をNetlify上でどう扱ったかを整理します。
2. SPAの画面遷移とサーバーのURL解決は別
React Routerで、
/
↓
/character
↓
/town
と遷移していると、複数のHTMLページが存在するようにも見えます。
しかし、SPAでは基本的に、
index.html
+
JavaScript
+
React Router
で画面を切り替えています。
つまり、
/character.html
/town.html
というHTMLファイルが個別に存在するわけではありません。
ここが、サーバー側のURL解決との違いです。
3. アプリ内遷移ならReact Routerが処理できる
例えば、タイトル画面から、
「はじめから」
↓
/character
へ進むとします。
このときは、すでにReactアプリが起動しています。
そのため、
React Router
↓
URLを解釈
↓
Character画面を表示
できます。
ブラウザは新しいHTMLファイルをサーバーへ要求しているわけではありません。
4. 直URLでは最初にサーバーへ問い合わせる
では、ブラウザのアドレス欄へ直接、
https://example.com/character
と入力した場合はどうでしょうか。
最初のリクエストは、
React Routerではなく、
ホスティング側
へ届きます。
サーバー側から見ると、
/character
というパスに対応するファイルを探そうとします。
そこに実ファイルがなければ、
404 Not Found
になる可能性があります。
5. 再読み込みでも同じ問題が起こる
直URLだけではありません。
例えばゲーム中に、
/town
を表示していたとします。
そこでブラウザの再読み込みを行うと、
今度は、
GET /town
がホスティング側へ送られます。
つまり、
アプリ内遷移
ではReact Routerが扱えたURLでも、
直URL
再読み込み
ではサーバー側の対応が必要になります。
6. SPA fallbackを用意する
そこで使うのが、
SPA fallback
です。
考え方は、
/character
/town
/saves
...
↓
実ファイルではない
↓
index.htmlを返す
↓
Reactを起動
↓
React RouterがURLを解釈
です。
これによって、
直URLでも再読み込みでも、最初にSPAを起動できます。
7. NetlifyではredirectでSPAへ戻す
今回の正式公開版はNetlifyで公開しています。
Netlify側では、SPA向けのredirectを設定して、
React Routerが扱うURLをindex.htmlへ戻します。
以下は考え方を説明するため、実装を簡略化しています。
/* /index.html 200
この設定の意図は、
未知のファイルを全部404にする
のではなく、
まずSPAを起動する
↓
URLの意味はReact Routerに判断させる
ことです。
8. fallbackしたから、すべてのURLを受け入れるわけではない
ここで注意したいのは、
SPA fallback
=
どんなURLでも正しい画面として受け入れる
ではないことです。
例えば、
/not-found-page
のような、アプリが知らないURLもindex.htmlへ届きます。
その後どうするかは、
React Router側の責務です。
9. 未知URLはタイトルへfail-closedする
このWeb RPGでは、未知URLについて、
*
↓
タイトル
へ戻す方針にしています。
つまり、
URLを理解できない
↓
適当な画面を表示する
のではなく、
安全な入口
↓
タイトル画面
へ戻します。
ここでも、セーブ設計で使ってきたfail-closedの考え方を使っています。
10. URLにも「今は成立しない状態」がある
URL自体は正しくても、その画面を表示するための状態が失われている場合があります。
例えば、
/result
のような結果画面です。
結果画面は、
直前に行った判定結果を前提にしています。
ところが、
/resultを直接開く
あるいは、
結果表示後に時間が経ってから再アクセスする
と、
必要な状態が存在しないことがあります。
11. ルートが存在することと、画面を表示できることは別
整理すると、
URLとして定義済み
=
いつでも表示可能
ではありません。
例えば、
/result
がReact Routerに登録されていても、
latestResolutionResult = null
なら、
その画面を正しく構成できない場合があります。
そのときも、
無理に空の結果画面を出すのではなく、
安全な画面へ戻します。
12. RouterとGameStateの両方で判断する
つまり、画面表示には、
URL
+
GameState
の両方が関係します。
例えば、
/town
↓
町へ戻れる状態か?
/event
↓
現在イベントが存在するか?
/result
↓
判定結果が存在するか?
といった確認です。
SPA fallbackはURLをReactへ届ける仕組みであって、
ゲーム状態の妥当性まで保証するものではありません。
13. 直URL対応は「ブックマークできる」とは限らない
例えば、
/character
は比較的独立した画面なので、直接アクセスしやすいURLです。
一方、
/event
/result
のような画面は、
ゲーム進行状態を必要とします。
そのため、
直URLでReactが起動する
ことと、
そのURLからゲームを再開できる
ことは分けて考えます。
14. /saves?mode=manageではqueryも残したい
セーブ画面では、
/saves?mode=manage
のようにquery parameterも使っています。
SPA fallbackでは、
単に/savesだけでなく、
?mode=manage
も含めた状態でReact Router側へ渡る必要があります。
これによって、
セーブデータ管理として開く
といった画面モードを再現できます。
15. productionで直URLを確認する
開発サーバーがSPA fallbackへ対応していても、
production hostingが同じ動きをするとは限りません。
そのため、
最終的にはNetlifyの公開URLで直接確認します。
今回の正式受入では、
/character
の直URL・再読み込み、
/saves?mode=manage
の直URL・再読み込み、
未知URLのSPA fallback
をproductionで確認しています。
16. 「トップから遷移できる」だけではテスト不足
例えば、
/
↓
ボタンを押す
↓
/character
が成功しても、
直URL問題は見つけられません。
なぜなら、その操作ではReact Routerだけで遷移できるからです。
そこで、
新しいブラウザ状態
↓
/characterを直接開く
というテストを別に持ちます。
17. 再読み込みも独立して確認する
さらに、
/characterへ到達
↓
reload
↓
Character画面が再表示される
ことも確認します。
直URLと再読み込みは仕組みとして似ていますが、
ユーザー操作としては別々に発生します。
18. Playwrightでproduction URLを直接開ける
Playwrightなら、
以下は考え方を説明するため、実装を簡略化しています。
test("characterを直URLで開ける", async ({ page }) => {
await page.goto("/character");
await expect(
page.getByRole("heading", {
name: "キャラクター設定",
})
).toBeVisible();
});
さらに、
以下は考え方を説明するため、実装を簡略化しています。
await page.reload();
await expect(
page.getByRole("heading", {
name: "キャラクター設定",
})
).toBeVisible();
とすれば、再読み込みも確認できます。
19. production URLでも同じ確認をする
ここで重要なのは、
localhostだけで終わらせないことです。
localhost
↓
PASS
でも、
Netlifyのredirect設定が間違っていれば、
productionでは失敗します。
そのため、
local hosting
↓
確認
production hosting
↓
同じ観点で再確認
とします。
正式公開版では、hosting関連の受入をlocal/productionの両方で行っています。
20. IndexedDBとも組み合わせて確認する
SPAの直URL対応だけ成功しても、
ゲームとしてはまだ足りません。
例えば、
町でセーブ
↓
タイトルへ戻る
↓
つづきから
↓
ロード
↓
町
という復帰導線があります。
この流れでは、
React Router
+
IndexedDB
+
GameState復元
が全部つながる必要があります。
21. URLが戻ってもGameStateが戻らなければ続きにならない
以前、ブラウザ再読み込み後に、
GameState初期化
↓
currentRoute = null
となり、
街道途中の状態を失ったことがありました。
つまり、
URLだけ復元
しても、
ゲーム状態が復元されない
なら続きから遊べません。
第28~29回で扱った起動時復元が、ここでも必要になります。
22. SPA fallbackと起動時復元は別の責務
整理すると、
Netlify SPA fallback
↓
Reactアプリを起動する
React Router
↓
URLから画面候補を決める
Save / GameState restore
↓
ゲーム状態を復元する
です。
この3つを混ぜずに考えます。
23. production受入では期限切れURLも確認する
正式公開版では、正常なURLだけではなく、
期限切れ結果URLのfail-closed
も確認しています。
例えば、
結果を表示するための状態がない
↓
/resultへアクセス
という場合です。
そこで、
空画面
壊れた画面
例外
にするのではなく、
安全な導線へ戻します。
24. 未知URLも同じ考え方で扱う
例えば、
/this-route-does-not-exist
でも、
Netlify側で404を出して終わるのではなく、
SPAを起動したあとReact Routerで、
unknown
↓
title
へ戻せます。
これはユーザー体験だけでなく、
アプリの状態を不明なまま進めないという意味でも役立ちます。
25. ただし静的ファイルまでfallbackさせない設計は必要
SPA fallbackでは、
公開するJavaScriptやCSS、画像などの静的ファイルもあります。
実際の設定では、
どのリクエストをindex.htmlへ戻すか
をホスティング構成に合わせて決める必要があります。
重要なのは、
React Routerが担当するURLを、ホスティング側の404で終わらせない
ことです。
26. HTTPSまで含めて公開環境になる
今回の正式公開版は、
Netlify上でHTTPS公開しています。
つまり、
React Router
SPA fallback
direct URL
reload
HTTPS
まで含めて、
Web公開環境として扱っています。
ローカル開発サーバーで動いただけでは、ここまでは確認できません。
27. 正式版ではhosting 7/7をlocalとproductionで確認した
正式公開判定では、
local production-hosting
7 / 7 PASS
production hosting
7 / 7 PASS
となっています。
確認項目には、
直URL、
再読み込み、
未知URL、
期限切れURL、
console/pageerror、
セーブからの復帰
まで含まれています。
28. 今回のポイント
今回のポイントは3つです。
- SPAではReact Router上のURLに対応する実HTMLファイルがないため、production hosting側にSPA fallbackが必要
- fallback後も、未知URLや成立しないゲーム状態を無理に表示せず、React側でfail-closedする
- localhostだけでなくproduction URLで直URL・reload・セーブ復帰まで確認する
整理すると、
Browser
↓
/character
↓
Netlify
↓
SPA fallback
↓
index.html
↓
React
↓
React Router
↓
Route / GameState validation
↓
画面表示
です。
今回の結論は、
SPAを公開するとは、トップページが表示されることではなく、URLを直接開いても再読み込みしても、アプリが安全に起動できること
です。
29. 次回
次回は、
localhostだけで終わらせない――production URLへ同じE2Eをかける【第43回】
です。
今回、
local
↓
production
の両方でhostingを確認すると書きました。
次回はさらに、
開発環境で使っていたPlaywrightのシナリオを、
production URL
に対しても再利用する方法を扱います。
見るのは、
productionだから
別のゲームとしてテストする
ことではありません。
同じ候補が、実際に公開された場所でも同じように動くことを確認する
ためのE2Eです。
この記事を最後まで読んでいただき、ありがとうございます。
「いいね」を押していただけるとうれしいです。これからの記事づくりの励みになります。
もし気に入っていただけましたら、フォローもよろしくお願いします。
前の記事
第41回 IndexedDBを直接扱わない――Dexie.jsでセーブRepositoryを組む
次の記事
第43回 localhostだけで終わらせない――production URLへ同じE2Eをかける
連載トップ
第0回 React+TypeScriptで異世界RPGを作る――連載の目的と開発ロードマップ
note関連記事
開発環境で動いたWebゲームを、本番環境へ届ける #24
noteでは、開発環境で動いていたWeb RPGをNetlifyへ公開し、React Routerの直URLや再読み込み、HTTPS、IndexedDBの保持まで確認した背景を、制作側の視点から書いています。