プロンプトと前提条件
あなたは複数ステップの申請フォームを担当しています。ユーザーはタブを切り替えたり、スマートフォンをロックしたり、戻る操作を行ったり、数時間ページを離れたりします。ブラウザはページをフリーズさせたり、bfcache から復元したり、メモリ逼迫時に破棄(discard)したりすることがあります。フォームは、より新しいサーバーデータを上書きすることなく、下書きを復元できなければなりません。
フォームには機密ではあるが規制対象外のデータが含まれており、ユーザーは複数のデバイスでサインイン可能で、ネットワークアクセスは断続的であると仮定します。回答では、何がベストエフォートであり、何が信頼できる唯一の情報源(authoritative)であるかを明記する必要があります。
面接官がテストしていること
- visibility、freezing、ページの破棄(discard)、bfcache による復元を区別できているか。
unloadやbeforeunloadを信頼できる永続化フックとして扱うのを避けているか。- ローカルの下書きに所有権、バージョン、有効期限、およびコンフリクトのルールがあるか。
- より新しいサーバーデータを無言で置き換えることなく、復元処理がユーザーの意図を保持しているか。
回答前の明確化のための質問
- ローカルの下書きが失われることは許容されますか、それともプロダクトとして永続的な復元保証を提供する必要がありますか? これにより、ローカルストレージが利便性のためのものか、単なるキャッシュに過ぎないかが決まります。
- 同じ下書きを複数のデバイスで編集できますか? 編集できる場合、サーバーにはバージョン管理またはコンフリクトポリシーが必要です。
- データはローカルに保存しても安全ですか? 機密性の高いフィールドには、暗号化、選択的除外、またはローカルへの非永続化が必要な場合があります。
- 送信(submit)の規約はどうなっていますか? 下書き保存と最終送信では、異なるべき等性とバリデーションルールが必要になります。
30秒の回答フレームワーク
「私はサーバーを正式な情報源(authoritative)とし、ローカルの下書きを制限付きの復旧用キャッシュとして扱います。入力の変更時にはデバウンスをかけてバージョン付きの下書きを永続化し、ドキュメントが hidden になった際にベストエフォートで更新をフラッシュします。bfcache による復元後の再検証には pageshow を使用し、フリーズ後の古い状態の更新には resume を使用します。unload に依存することは決してありません。各下書きには有効期限、スキーマバージョン、ベースとなるサーバーバージョン、およびダーティなフィールドを記録し、競合は無言で上書きするのではなく、明示的に表示またはマージします。」
ステップバイステップの詳細解説
1. ライフサイクル状態のモデル化
visibilitychange は、ページが hidden または visible になったことを伝えます。JavaScript の実行が継続されることを保証するものではありません。ブラウザは hidden なページをフリーズさせることがあり、破棄されたページはクリーンアップコードを一切実行しない可能性があります。pagehide と pageshow はナビゲーションと bfcache の復元を表し、freeze と resume は Chromium のライフサイクル遷移を公開します。
したがって、設計では直前の unload ではなく、リスクが発生する前に保存します。visibilitychange から hidden への遷移時には、小さなローカル書き込みとベストエフォートのネットワークフラッシュをスケジュールします。pagehide では、重要でない処理を停止し、ローカルのチェックポイントを記録します。pageshow では、event.persisted を確認し、復元されたフォームを表示する前にサーバーバージョンを再検証します。
2. ローカルの下書きを制限付きかつバージョン管理されたものにする
安全に復元できるフィールドのみを保存します。下書きレコードには以下を含めることができます:
draft_id, user_id, form_schema, base_server_version,
changed_at, expires_at, dirty_fields, values構造化データには IndexedDB を使用し、検索には小さなローカルインデックスを使用します。キー入力ごとに書き込みが発生しないように入力をデバウンスし、ペイロードサイズに上限を設け、期限切れの下書きを削除します。スキーマバージョンを保持することで、アプリは互換性のないレコードを現在のデータとして解析する代わりに、移行または破棄できます。
ローカルレコードは決定権を持つものではありません(authority ではない)。これはブラウザによって失われたり、古くなったり、重複したり、削除されたりする可能性があるユーザーデバイス上のキャッシュです。
3. ページが hidden になった際に安全にフラッシュする
ドキュメントが hidden になったときは、まずローカルのチェックポイントをコミットし、プロダクトがバックグラウンド保存をサポートしている場合は、小さな認証済みリクエストを試行します。リクエストには、下書き ID とベースにしたサーバーバージョンが含まれます。サーバーはバージョンが一致した場合にのみ受け入れ、新しいバージョンを返します。
時間のかかるリクエストでナビゲーションをブロックしてはいけません。リクエストが完了できなくても、ローカルチェックポイントによってこのデバイスは保護されます。同期的な unload リクエストは避けてください。ナビゲーションを阻害し、モバイルのライフサイクルパスでは保証されません。
4. bfcache とフリーズからの復元
pageshow が persisted で発火したとき、ページには視覚的には完全でも論理的には古いスナップショットが含まれている可能性があります。現在のサーバーバージョンを取得し、フォームのベースバージョンと比較して、別のデバイスが下書きを変更した場合は競合の選択肢を表示します。resume が発火したときは、ページがフリーズしている間に期限切れになった可能性のあるセッションとデータをリフレッシュします。
ページが破棄された場合、再開するためのメモリ内状態は存在しません。次回の読み込み時に、期限切れになっていないローカルの下書きを検索し、そのベースバージョンをサーバーと比較して、復元、破棄、またはマージを提案します。ユーザーはマージを受け入れる前に、どの値がより新しいかを確認できる必要があります。
5. 失敗パスとプライバシーのテスト
タブの切り替え、モバイルアプリの切り替え、戻る/進むナビゲーション、bfcache の復元、ブラウザの破棄、オフライン編集、クォータ超過、スキーマ移行、期限切れの下書き、および 2 台のデバイス間の競合をテストします。より新しいサーバーバージョンが決して無言で上書きされないことをアサートします。
ログから機密フィールドをマスクします。アカウント削除時には下書きをクリアし、短い保持期間を遵守し、プライバシー通知でローカル復元について説明します。復元の成功率、コンフリクト率、ローカル書き込みの失敗、およびサーバー保存のレイテンシを計測します。「保存済み」インジケーターは、unload ハンドラが実行されたという期待ではなく、確認されたチェックポイントを表す必要があります。
質の高い回答例
私はサーバーをバージョン管理された信頼できる情報源とし、復旧用に制限付きのローカル下書きを保持します。入力の変更は、スキーマバージョン、有効期限、ダーティフィールドのセット、およびベースとして使用されたサーバーバージョンとともに、デバウンスして IndexedDB に保存されます。visibilitychange が hidden を報告したとき、チェックポイントを書き込み、小さな認証済み保存を試みますが、ナビゲーションをブロックしたり unload に依存したりはしません。
pageshow、特にイベントが bfcache から発生した場合は、復元されたフォームを信頼する前にサーバーバージョンを再取得します。resume では、フリーズ中に期限切れになった可能性のあるデータとセッション状態をリフレッシュします。破棄されたページは新規読み込みから開始し、期限切れでないローカル下書きを提案します。バージョンが異なる場合は、競合 UI またはフィールドレベルのマージを表示します。新しいサーバーのコピーを決して無言で上書きすることはありません。テストでは、モバイルの破棄、オフライン編集、クォータ制限、および 2 台のデバイスの競合をカバーします。
よくある間違い
- 間違い:
beforeunloadでのみ保存する → 失敗する理由: モバイルブラウザはそれを発火させずにページを終了することがあり、bfcache をブロックする可能性がある → 修正方法: 入力時および hidden への遷移時にチェックポイントを作成する。 - 間違い: 表示されている bfcache ページを最新として扱う → 失敗する理由: スナップショットに古いサーバー状態が含まれている可能性がある → 修正方法:
pageshowで再検証し、バージョンを比較する。 - 間違い: ローカルストレージを信頼できる情報源(authority)にする → 失敗する理由: クリアされたり、古くなったり、利用できなくなったりする可能性がある → 修正方法: サーバーバージョンを使用し、ローカルデータは復元の候補として提示する。
- 間違い: 新しいバージョンに対してフォーム全体を再試行する → 失敗する理由: 無関係な編集内容を上書きしてしまう → 修正方法: オプティミスティックなバージョンとともにダーティなフィールドを送信し、競合を解決する。
- 間違い: デバッグのために下書きの値をログ出力する → 失敗する理由: 機密性の高いコンテンツがテレメトリに漏洩する → 修正方法: ID、バージョン、サイズ、および結果のみをログ出力する。
フォローアップの質問と回答
localStorage と IndexedDB のどちらを使うべきですか?
localStorage は、ごく小さな同期的メタデータにのみ使用してください。IndexedDB は、構造化された下書き、より大きなペイロード、および非同期の書き込みに適しています。どちらも完全な永続性やデバイス間の決定権を提供するものではありません。両者とも有効期限、クォータ処理、スキーマのバージョニング、およびプライバシールールが必要です。
ユーザーが 2 つのデバイスでオフライン編集した場合はどうなりますか?
各保存にベースサーバーバージョンとデバイスまたは下書き ID を付与します。接続が回復したときは、一致するバージョンのみを受け入れ、フィールドレベルのマージを表示するか、ユーザーに選択させます。ラストライターウィン(最後の書き込み優先)ポリシーは、プロダクトが無言のデータ消失を明示的に許容し、各フィールドが独立している場合にのみ許容されます。
Service Worker は最終保存を保証できますか?
いいえ。Service Worker は再試行の配信を改善できますが、停止されたり、ネットワークアクセスを失ったり、特定のフローで利用できなかったりすることがあります。UI は確認済みのチェックポイントを通知する必要があり、サーバーは再試行をべき等にする必要があります。リクエストを完了できないデバイスにおいて、ローカルの下書きは依然としてフォールバックとして機能します。