プロンプトとコンテキスト
ブラウザ履歴を損なうことなく、復旧可能かつオブザーバブルな SPA ナビゲーション層をどのように再設計しますか?読み込みの競合、エラー復旧、スクロール復元、および Navigation API が利用できない場合のフォールバックについて説明してください。
この質問はフロントエンド、フルスタック、および Web プラットフォームの職種に適しています。暗記されたフレームワークの設定ではなく、ブラウザのナビゲーションモデル、非同期ステートマシン、およびプログレッシブエンハンスメントを検証します。Navigation API は、navigate、navigatesuccess、navigateerror などのイベントや intercept() を通じて、同一ドキュメントナビゲーションの統合されたビューを提供します。これは、サーバーレンダリングされた初期ロードを置き換えるものではなく、クロスドキュメントナビゲーションのセキュリティ境界を変更するものでもありません。
面接官が評価するポイント
- コンポーネントのメモリだけでなく、URL と履歴エントリをナビゲーション状態として扱うこと。
- 同一ドキュメントのルートを、ドキュメント、ダウンロード、フォーム、およびクロスオリジンリンクから区別すること。
- 古いリクエスト、キャンセル、エラー、および戻る/進むナビゲーションの処理。
- スクロール、フォーカス、およびページ状態の復元ルールの定義。
- 初回ロードおよび非対応ブラウザの機能性の維持。
- 成功、失敗、タイムアウト、キャンセル、および復旧の計測。
30秒での回答
「URL と履歴を信頼できる唯一の情報源(source of truth)として、サーバーレンダリングされたエントリポイントと通常のリンクが機能し続けるようにします。Navigation API が利用可能な場合は、同一オリジンのアプリケーションルートのみをインターセプトし、navigate イベントでキャンセル可能な読み込みを開始して、新しいナビゲーションが古いナビゲーションをキャンセルできるようにします。現在のタスクのみがビューをコミットできます。成功時にはスクロールとフォーカスを復元し、失敗時には古いビューを維持して再試行または再読み込みを提供します。非対応ブラウザは既存の History API パスを使用し、同じローダーとメトリクスを共有します。」
ステップバイステップの解決策
ステップ 1: インターセプトするナビゲーションを制限する
遷移先が同一オリジンであり、アプリケーションルートであり、現在のドキュメントでレンダリングしても安全であるかを確認します。外部リンク、ダウンロード、クロスオリジンの遷移先、特殊プロトコル、およびフォームのセマンティクスは、ブラウザのデフォルトの動作を維持する必要があります。最初のドキュメントリクエストは依然としてサーバーとブラウザに属しており、navigate イベント単体で失敗したスクリプトのロードを復旧可能にすることはできません。
承認されたアプリケーションルートに対してのみ intercept() を呼び出します。ハンドラーはデータを読み込み、ビューを更新し、スクロールポリシーを適用します。それ以外のすべての遷移先については、ブラウザが通常通りナビゲートできるようにします。これによりリンクのセマンティクスが維持され、プラットフォームのナビゲーションが偶発的なクライアント専用のステートマシンになってしまうのを防ぎます。
ステップ 2: ナビゲーションをキャンセル可能にする
すべてのナビゲーションに増加するシーケンス番号を割り当て、その AbortSignal をデータローダーに渡します。ユーザーが /search?q=a から /search?q=ab に移動した場合、最初のリクエストからの遅れて届いたレスポンスが2番目のリクエストを上書きしてはなりません。コミットする前に、シグナルが中断されていないこと、シーケンスが最新であること、および遷移先が依然として意図した状態と一致していることを確認します。
let latestNavigation = 0;
navigation.addEventListener("navigate", (event) => {
if (!event.canIntercept || !isAppRoute(event.destination.url)) return;
const id = ++latestNavigation;
event.intercept({
async handler() {
const data = await loadRoute(event.destination.url, event.signal);
if (event.signal.aborted || id !== latestNavigation) return;
renderRoute(data);
}
});
});この例は競合ルールを示すものであり、完全なルーターではありません。本番コードでは、ローダーのエラー、タイムアウト、キャッシュヒット、およびコンポーネントの破棄(teardown)に対処する必要があります。「最後のレスポンスが勝つ」というのは正しくありません。なぜなら、ネットワークの完了順序はユーザーの最新の意図とは限らないからです。
ステップ 3: 履歴、スクロール、およびページ状態のコミット
ナビゲーションが成功した後、遷移先 URL を結果としてコミットします。小さな復旧可能な値が現在の履歴エントリに属している場合、updateCurrentEntry() に保存できます。大きなオブジェクトや機密データはそこには属しません。スクロールポリシーは、新しいルート、戻る/進む、およびアンカーを区別する必要があります。通常、新しいルートは最上部から始まり、履歴の移動では保存された位置が復元されます。
UI が明示的に保留状態を表している場合を除き、読み込みの開始時にタイトル、選択されたアイテム、またはパンくずリストを変更しないでください。成功後にメインの視覚的状態をコミットし、失敗後は再試行アクションとともに古いコンテンツを維持します。navigatesuccess と navigateerror は、レイテンシ、キャンセル、および失敗のメトリクスを収集するための有用な観測ポイントです。
ステップ 4: エラーからの復旧
読み込みが失敗した場合でも、古いページが使用可能なままである必要があります。再試行、戻る、および完全な再読み込みアクションを提供し、認証、認可、リソースの欠落、および一時的なネットワーク障害を区別します。遷移先が完全なドキュメントレスポンスを必要とする場合は、インターセプトを停止し、ブラウザに処理させます。
クライアントの状態やスクリプトの実行が破損している場合でも、通常のリンクやサーバールートから URL を基にページを再構築できるようにする必要があります。復旧をインメモリキャッシュに依存させないでください。遷移先、ナビゲーションシーケンス、エラークラス、およびフォールバックが発生したかどうかを記録して、「アドレスは変更されたが古いページが残った」という問題が診断できるようにします。
ステップ 5: プログレッシブエンハンスメントの適用
Navigation API のサポートはベースラインの History API よりも新しいため、これだけを唯一の経路にすることはできません。window.navigation と実際に使用するメソッドを検出し、拡張されたパスを選択します。非対応ブラウザでは、既存の History API またはフレームワークルーターを通じて同じルートローダーを再利用する必要があります。単に互換性のためだけにビジネスルールを分岐させてはなりません。
初回ロード、更新、戻る/進む、中断されたリンク、およびスクリプト障害を両方の機能パスでテストします。低速ネットワーク、高速な連続ナビゲーション、クロスオリジンリンク、およびダウンロードを含めます。機能検出は、壊れやすいブラウザのバージョンリストではなく、機能の使用箇所に近い場所で行う必要があります。
ステップ 6: パフォーマンスとアクセシビリティの検証
検索、フィルタリング、戻る、更新、ディープリンク、再試行などの実際のタスクを使用します。ブラウザの機能ごとにグループ化して、インタラクティブになるまでの時間、キャンセル率、失敗率、フォールバック率、および重複リクエストを測定します。コンポーネントのレンダリング時間だけでは、データ、権限、または履歴の障害を説明することはできません。
ネイティブなリンクセマンティクス、キーボード動作、およびアクセシブルなフォーカス移動を維持します。インターセプトされたナビゲーションの後、ドキュメントのタイトルを更新し、フォーカスをメインコンテンツに移動します。新しいタブでリンクを開く操作を妨げないでください。キャッシュはレイテンシを短縮する可能性がありますが、ページが復旧できる唯一の情報源であってはなりません。
情報の利得と境界
有用な違いは、Navigation API がプラットフォームのナビゲーションイベント、履歴、およびキャンセル可能な読み込みを1つの状態チェーンに結合することです。データソースの信頼性を高めたり、クロスオリジンページを同一ドキュメントルートに変換したりするわけではありません。優れた面接の回答では、インターセプトの境界、コミットポイント、失敗時の古いビューの動作、およびブラウザに機能がない場合のデフォルト動作が明確に述べられます。
模範解答
「ナビゲーションを同一オリジンのアプリケーションルート、完全なドキュメント、ダウンロード、フォーム、およびクロスオリジンの遷移先に分類し、最初の分類のみをインターセプトします。URL と履歴を信頼できる唯一の情報源として、サーバーのエントリポイントと通常のリンクは有効なまま維持されます。Navigation API を使用する場合、navigate イベントが intercept() を呼び出し、シーケンス番号とキャンセルシグナルを作成します。新しいナビゲーションは古い読み込みをキャンセルし、結果が依然として最新である場合にのみコミットされます。
成功後は、ビュー、タイトル、フォーカス、およびスクロールポリシーを更新します。新しいルートは最上部から開始し、戻る/進むでは履歴の位置が復元されます。復元可能な小さな状態は現在の履歴エントリを使用できますが、大きなデータや機密データは別の場所に保持します。失敗時には古いビューを維持し、再試行、戻る、または完全な再読み込みを提供し、ナビゲーションイベントとサーバーログを通じてレイテンシ、キャンセル、およびエラークラスを記録します。
Navigation API が利用できない場合、同じローダーが History API またはフレームワークルーターを介して実行されます。外部リンクとダウンロードは常にデフォルトのブラウザ動作を使用します。ディープリンク、更新、素早いクリック、低速ネットワーク、クロスオリジンリンク、スクリプト障害、および支援技術のタスクを検証し、アドレス、コンテンツ、フォーカス、履歴が決して不一致にならないようにします。その結果、新しいブラウザでのみ動作するクライアント専用の代替物ではなく、プログレッシブなナビゲーション層が実現します。」
よくある間違い
- すべてのリンクをインターセプトする → ダウンロード、フォーム、クロスオリジンのセマンティクスが壊れる → 承認された同一オリジンのアプリケーションルートのみをインターセプトする。
- 最後のレスポンスを優先する → 古いデータが現在の意図を上書きする可能性がある → キャンセルとシーケンスチェックを使用する。
- レンダリングのみをテストする → データ、権限、および履歴の障害が隠れたままになる → ナビゲーションタスク全体と復旧をテストする。
- Navigation API を初回ロードのインフラとして扱う → 更新やスクリプトの失敗が復旧不能になる → サーバーレスポンスと通常のリンクを維持する。
- すべての状態を履歴エントリに配置する → エントリが肥大化するか、機密データが公開される → 再構築可能な小さな状態のみを保存する。
- フォールバックのためにビジネスロジックを重複させる → 拡張パスとフォールバックパスの間で乖離が生じる → ローダー、状態ルール、およびメトリクスを共有する。
フォローアップの質問
古いリクエストがすでにキャッシュに格納されている場合はどうなりますか?
キャッシュへの書き込みは許可される場合がありますが、UI のコミットには依然として最新のシーケンスと中断されていないシグナルが必要です。URL、パラメータ、およびバージョンでエントリをキー指定します。遅れて届いた結果は将来のリクエストを温める(ウォームアップする)可能性がありますが、現在のページを変更してはなりません。
認可エラーの場合は前のページに戻るべきですか、それともログインにリダイレクトすべきですか?
明示的なサーバーステータスを使用して、未認証、未認可、および存在しないリソースを区別します。未認証のユーザーはリターン URL 付きでログイン画面に遷移でき、未認可のユーザーには説明と安全な次のアクションが必要です。認可エラーを一般的なネットワークエラーとして偽装しないでください。
Navigation API のないブラウザはどのようにテストしますか?
機能検出を介して同じディープリンク、更新、戻る/進む、低速ネットワーク、および失敗のタスクを実行し、URL、コンテンツ、タイトル、フォーカス、およびスクロールを確認します。重要なのは動作の同等性であり、内部イベントの順序が同一である必要はありません。
なぜフレームワークのルーターに完全に依存しないのですか?
フレームワークのルーターを再利用することは可能ですが、回答ではブラウザの境界を定義する必要があります。すなわち、どのナビゲーションをネイティブのままにし、どれを拡張できるか、そしてキャンセル、履歴、エラーがフレームワークイベントにどのようにマッピングされるかです。プラットフォームのセマンティクスから始めて、アダプターについて説明してください。