はじめに
Fieldnoteのキーワード、カテゴリ、並び順、ページはすでにURLで伝えられます。準備済みコントローラーは検索のたびに現在の履歴項目を置き換えます。そのため直接リンクは機能しますが、前のビューは失われ、いくつか検索した後にBackで確実に戻れません。新しいユーザー操作と復元を区別し、ブラウザー自身の履歴を使えるようにします。
この独立したラボでは、前のラボで完成した選択・正規化・URLファイルが用意され、前のVMは不要です。前のラボの4状態のルールと、JavaScriptの関数、オブジェクト、DOM選択、イベントの基礎が前提です。pushState、replaceState、popstate、そして共有に古いローカル設定やメモリ上の履歴オブジェクトではなくURLが必要な理由を学びます。非同期リクエストやフレームワークのルーターは使いません。40分は作者の見積もりです。
Workspace と Fieldnote を使います。編集は /home/labex/project/fieldnote/work/app.js だけです。selection.js、state.js、url.js、catalog.js、view.js、HTML/CSS、独立したStudyビューとサービスを保持します。レンダラーは準備済みで、選んだイベント自身のタイトル、説明、日付、時刻、場所を表示します。サイトは英語のままです。リンクはこのVMの公開経路が利用できる間だけ開けます。インターネットへの公開ではありません。
確定したユーザー変更を履歴に追加する
このステップでは、初期化を1つの履歴項目に保ち、読者が別の検索やページを確定したときに新しい項目を作ります。
Fieldnote で Open Study view in a new page を選びます。これは製品の埋め込みタブではなく独立したブラウザーページです。print & fold、Make、Soonest first の2ページ目を読みます。Previous を選び、Keyword を sound + light、Category を Listen、Sort by を Title A–Z にして Search を実行します。ページは1に戻ります。サイトの Previous ではなく、ブラウザーの Back を使います。以前のキーワード、カテゴリ、並び順、ページ、完全な一致レコードが戻ります。ブラウザーの Forward で新しいビューに戻します。これは正常な例であり、あなたの作品の完成にはなりません。
以下は編集前の独立したStudyビューです。その共有hrefは同じ可視ページを表しますが、Workを完成させるものではありません。

Studyを閉じ、Fieldnote の入口で Open Work view in a new page を選びます。作業ファイルにはG1の全関数がありますが、コントローラーはまだ項目を置換するだけです。Workspace に切り替えます。cd はこの永続ターミナルのディレクトリを設定し、cat はファイルを変更せずに読みます。
cd /home/labex/project/fieldnote/work
cat app.js
現在の show は normalize を呼び、stateURL でURLを作り、アドレスを置換し、draw で選択レコードを表示します。補助関数は準備済みの state.js、url.js、view.js、selection.js にあります。この順序を保ちながら、ビューを表示する2つの理由を区別します。
history.pushState(null, '', url) は同一オリジンの履歴項目を追加し、新ページを要求せずにアドレスを変えます。replaceState は現在項目だけを更新します。最初の引数は任意の関連状態です。必要な状態はURLにすべてあるので null を使います。空文字列はAPIが使用しないタイトル引数です。どちらの呼び出し自体も popstate を発生させません。実際の履歴移動は次のステップで扱います。初期正規化はreplaceを使うため、リンクを開く通常の移動に加えて初期化が余分な項目を増やしません。検索とページボタンは確定URLが変わる場合にpushを使い、同じビューの再送信で重複を増やす必要はありません。
document.querySelector('form') は準備済みフォームを探します。form.elements.q は q という名前の欄にアクセスします。preventDefault は標準のフォーム移動を止め、コントローラーが表示を更新できるようにします。イベントリスナーは指定イベント時に実行されます。スプレッド構文 {...current, page: ...} は現在状態の各フィールドをコピーしてページだけを上書きし、確定済みフィルターを保ちます。欄を編集しただけでは Search まで下書きです。ページボタンは確定状態を使います。
次の完全な置換内容を保存します。cat > app.js はそのファイルだけを置換します。引用符付きヒアドキュメント <<'JS' は内容をそのまま保持し、1行に JS だけを書いて終了します。
cat > app.js <<'JS'
const form = document.querySelector('form');
let current;
function show(raw, mode) {
current = normalize(raw);
const url = stateURL(current);
if (mode === 'push' && url.href !== location.href) {
history.pushState(null, '', url);
} else if (mode === 'replace') {
history.replaceState(null, '', url);
}
draw(current, matches(current));
}
form.addEventListener('submit', event => {
event.preventDefault();
show({q: form.elements.q.value, category: form.elements.category.value,
sort: form.elements.sort.value, page: 1}, 'push');
});
document.querySelector('#previous').addEventListener('click', () =>
show({...current, page: current.page - 1}, 'push'));
document.querySelector('#next').addEventListener('click', () =>
show({...current, page: current.page + 1}, 'push'));
show(readURL(), 'replace');
document.documentElement.dataset.ready = 'true';
JS
&& は両方の条件を必要とするため、pushを要求し、かつアドレスが異なる場合だけ項目を作ります。else if は初期化で置換を選びます。準備済みファイルは引き続き正規化とコースの4キーだけを管理し、実際のプロキシパス、追加クエリ、フラグメントを保ちます。末尾の data-ready は初期描画完了の目印です。HTMLの欄が現れただけでJavaScript完了と考えないでください。
独立した作業ページに戻り再読み込みします。print & fold、Make、Soonest first を検索し、Next を選びます。2ページ目の地図版と North room のイベントを見ます。sound + light、Listen、Title A–Z に変えて送信すると、1ページ目に別々の音のイベント2件が表示されます。Open this view in a new page を実行し、新ページを再読み込みします。Title A–Z と正しい日付・場所が保持される必要があります。キャッシュした設定は不要です。既定以外の並び順も含む4つの確定値がURLに必要です。新しいページを閉じ、元の作業ページを残します。
まだBackで作業ページが描き直されるとは期待しないでください。新しい項目はありますが、復元リスナーは未実装です。このステップの Check を実行します。直接URL、有用な新項目、初期化時の置換を確認し、次のリスナーはまだ要求しません。JavaScriptのタイプミスで更新が止まったら cat app.js を使い、完全なコードと比較して置換し、再読み込みします。修正保存で準備カタログは消えず、VMのリセットも不要です。
本物のBackとForwardでビューを復元する
このステップでは、実際の履歴移動後にURLを読み、別のブラウザーコンテキストでも同じビューを再構成できることを確かめます。
BackとForwardは現在の履歴項目を変えます。ブラウザーが該当する同一文書の項目を移動すると popstate を発生させます。自分で pushState を呼んでも発生しません。実際のイベントを受け、現在のURLを読み、正規化して描画します。リスナーからpushしないでください。既存項目を復元する代わりに新しい履歴項目を追加してしまいます。直接リンクなど、メモリに event.state がなくてもURLは動作する必要があります。localStorageを正本にしたり、並び順を履歴オブジェクトだけに保存したりしないでください。
Workspace に切り替えます。前のステップで保存したファイルにリスナーを追加します。> と違い、>> は既存内容を削除せず追記します。このブロックは1回だけ実行します。誤って2回追記したら、前の完全なコードで app.js を置換し、改めて1回追記します。
cat >> app.js <<'JS'
window.addEventListener('popstate', () => show(readURL(), 'replace'));
JS
アロー関数のコールバックはネイティブな履歴移動時に動きます。同じ show を使うため、欄、レコード順、ページ表示、無効境界、実際の共有hrefが新しい現在URLに一致します。置換で同等入力を正規化しても構いませんが、項目を追加してはいけません。学習フローではイベントを人工的に発行せず、見えないテストの近道も使いません。
元の独立作業ページに戻ります。明確な初期項目を用意するため、アドレスバーから実際の /work/ URLを次のクエリで開き、実際のオリジンとパスを保ちます。
?edition=november&q=print%20%26%20fold&category=make&sort=title&page=2#directory-title
これが状態Aです。Title A–Z、2ページ目、Print & Fold: map edition、次に Print & Fold: tiny type です。時刻と場所を読みます。Previous で状態Bにします。1ページ目は通常の Print & Fold 2件と Print & Fold: colour です。sound + light、Listen、Soonest first を送信して状態Cにします。1ページ中1ページ目で、日付の異なる2件です。操作を通じて edition とフラグメントを保ちます。フィルター変更でページは先頭に戻ります。
ブラウザーの本当の Back を2回使い、C → B → Aと戻ります。各項目で全コントロール、ページ、順序付きタイトル、各イベントの日付・時刻・場所を比べます。次に Forward を2回使い、A → B → Cと進みます。このBack/Back/Forward/Forwardをもう1回繰り返します。復元でこっそり項目を増やしたり、進む側の履歴を失ったりしてはいけません。製品タブ切り替えは履歴移動ではなく、サイトの Previous は結果ページ操作で、ブラウザーBackではありません。
Cで実際の Open this view in a new page リンクを開きます。新ページは同じ確定状態を表示する必要があります。再読み込みしてもう一度比べます。検証器は旧localStorageや履歴状態のないクリーンなコンテキストでも、生成した正確なリンクを開きます。通常のブラウザーでの新ページは別タブであり、全ストレージが隔離された保証ではありません。クリップボードAPIや未検証の Copied 表示は不要です。
元のページへ戻り、BackでBに移動して再読み込みします。URLを読むためBが維持されます。変更したが送信していないキーワードは下書きで、再読み込み時に破棄されます。unsubmitted draft と入力して Search を押さずに再読み込みし、Bの確定キーワードが戻ることを明示的に試します。通常の間違いとして prnit を送信し、正しいゼロ件メッセージを読み、print & fold、Make に修正して送信します。空の結果に偽の代替カードを表示してはいけません。
デスクトップ幅、約768ピクセル、約390ピクセルで、完全なカード、欄、共有リンクを比べます。Tab と Enter でSearchを実行し、見やすいフォーカスを確認します。準備済みレスポンシブ外枠を保ち、Check を実行します。検証は変更されていないあなたのフロントエンドを隔離環境で実行し、本物のBack/Forwardとクリーンなコンテキストでリンクを開く操作を試し、各可視レコードを独立カタログと比較して、現場の作業領域を保持します。アカウントを作ったり、恒久サイトを公開したりはしません。
まとめ
新しい確定操作を、初期化や履歴からの復元と分けました。本物のBack/Forward、直接共有リンク、再読み込みが、同じURL駆動状態と完全なイベントレコードを使うようになりました。チャレンジではルーター、バックエンドページング、非公開ストレージへの依存を増やさず、別のカタログとページ件数へ移します。
