JavaScript約10分

URLの検索条件を読み取る:未指定・空欄・重複を分けて復元する

検索結果のURLを開き直したら、空の検索語まで初期表示へ戻っていませんか。架空の展示資料検索で、URLSearchParamsから未指定・空欄・重複を読み分け、復元する状態と期待結果を決める練習です。

この記事の目次
  1. 検索語が「ない」と「空」を、先に書き分ける
  2. 四つの状態を返す関数を作る
  3. 状態を、検索前の案内文へつなぐ
  4. 空欄まで初期表示になるなら、既定値を入れる場所を見る
  5. URL全体を渡した失敗は、検索部分の取り出しから直す
  6. 記号を含むURLでも、案内文と検索語が合うか試す

検索したページのURLを開き直すと、条件が消えたり、空欄なのにおすすめが出たりする。URLの検索条件を読み取るときは、取り出した文字列だけでなく「条件が指定されていたか」も見ます。初期表示と検索を分ける約束を、短い関数へまとめましょう。

題材は架空の展示資料検索です。JavaScriptの関数、if文、オブジェクトを少し読める人向けに、URLから画面用の状態を作る部分を扱います。実際の検索、画面への描画、URLを書き換える処理は含めません。Node.js、または自分の練習ページのコンソールで実行できます。

検索語が「ない」と「空」を、先に書き分ける

URLのうち、疑問符の後ろにあるq=地図のような名前と値の組を、検索パラメーターと呼びます。qという名前はこの練習で検索語に割り当てる約束です。URL自体が、その名前の意味を決めてくれるわけではありません。

今回はqがなければ初期表示、qがあるが空なら入力の案内、文字があれば検索用の状態にします。同じqが二つ以上あれば、どちらを採るか推測せず、指定を一つに直す案内を返します。たとえば次の四つは異なる入力です。

検索部分 戻す状態 次の表示に渡す内容
なし initial おすすめ資料の表示
?q= empty 検索語を入力する案内
?q=地図 search 検索語「地図」
?q=地図&q=写真 invalid 検索語の重複を直す案内

初期表示でおすすめを出すのは、今回の画面設計です。未指定も空欄も全件表示にするサイトなら、その二つを同じ状態へまとめる選択もあります。先に区別して読み取ることで、意図した統合なのか、値を失った結果なのかを説明できます。

四つの状態を返す関数を作る

MDNのgetの説明では、指定名がなければnull、あれば文字列が返ることを確認できます。nullは「値が得られなかった」を表し、空文字とは別です。getAllの説明では、同じ名前の値を配列で取り出せるため、重複を先に調べられます。

次をread-search.jsとして保存し、同じフォルダでnode read-search.jsを実行します。searchにはURL全体ではなく、先ほどの表のような検索部分の文字列を渡します。

function readSearchState(search) {
  const params = new URLSearchParams(search);
  if (params.getAll("q").length > 1) {
    return { status: "invalid", query: "" };
  }

  const raw = params.get("q");
  if (raw === null) {
    return { status: "initial", query: "" };
  }

  const query = raw.trim();
  if (query === "") {
    return { status: "empty", query: "" };
  }
  return { status: "search", query };
}

for (const search of ["", "?q=", "?q=地図", "?q=地図&q=写真"]) {
  console.log(readSearchState(search));
}

出力のstatusは順にinitial、empty、search、invalidです。三番目のqueryだけが「地図」で、ほかは空文字になります。statusは表示側が何をするか選ぶための、この例で決めた名前です。emptyやinvalidを返したら検索を実行せず、それぞれの案内を選びます。

処理は重複確認、未指定確認、文字の整形、空欄確認の順です。nullの段階でtrimを呼ばないことも、この順序の理由になります。前後の空白だけを除く判断はtrimの練習記事で扱っています。ここでは空白だけのqもemptyへまとめます。

q以外の名前はこの関数では使いません。カテゴリやページ番号を足す場合は、取り出すだけで済ませず、使ってよい値や範囲を別に決めます。未知の値を受け取ったからといって、検索結果が0件だったと判断する処理ではありません。

状態を、検索前の案内文へつなぐ

返されたstatusを実際に使う小さな例として、案内文を作ります。同じファイルの末尾へ次を足してください。画面の描画は行わず、表示する文言をコンソールで確認します。

function searchMessage(state) {
  if (state.status === "initial") return "おすすめ資料を表示します";
  if (state.status === "empty") return "検索語を入力してください";
  if (state.status === "invalid") return "URLのqは一つだけ指定してください";
  return `「${state.query}」を検索します`;
}

for (const search of ["", "?q=", "?q=地図", "?q=地図&q=写真"]) {
  console.log(searchMessage(readSearchState(search)));
}

出力は順に次の四行になります。

おすすめ資料を表示します
検索語を入力してください
「地図」を検索します
URLのqは一つだけ指定してください

searchMessageに渡すのはreadSearchStateの戻り値だけ、という前提です。外から届く任意のオブジェクトを検査する関数ではありません。

ここで「検索します」と出ても、検索結果が見つかったわけではありません。状態の読取りと、資料を検索する処理は別だからです。実際の検索を足す際はstatusがsearchのときだけqueryを渡し、0件かどうかはその結果で判断します。

空欄まで初期表示になるなら、既定値を入れる場所を見る

一つ目の失敗は、params.get("q") || "おすすめ"のように、読み取った直後に既定値を入れることです。qがない場合も、qが空文字の場合も右側が選ばれ、二つの入力が同じ「おすすめ」に置き換わります。その後で分岐しようとしても、もとの違いは分かりません。

修正は、完成例のようにrawをそのまま受け取り、まずnullと比較することです。空文字までまとめて扱うif (!raw)も今回は使いません。再確認では空文字の検索部分と?q=を一つずつ渡し、initialとemptyに分かれるかを見ます。

空文字を残すだけなら??という演算子で既定値を補う書き方もありますが、今回ほしいのは文字列の補充ではなく、表示状態の区別です。短い式へまとめる前に、何の状態を残すかを決めてください。

同じく、重複したqへgetだけを使うと先頭の値が返り、二つ指定された事実を見落とします。複数のタグを指定できる検索なら複数値を受け取る設計もありますが、今回は検索語一つという約束なのでinvalidにします。順序を逆にした二つのURLも、ともにinvalidになることを確かめます。

URL全体を渡した失敗は、検索部分の取り出しから直す

二つ目の失敗は、完成例にhttps://example.com/archive.html?q=地図をそのまま渡すことです。qがあるはずなのにinitialになる場合があります。MDNのURLSearchParamsの説明にあるとおり、この処理はURL全体を分解するものではありません。

開いているページならreadSearchState(window.location.search)で検索部分を渡します。これはブラウザー用の呼び出しです。Node.jsにはwindowがないため、保存した練習ファイルの実行時には使いません。

URLの文字列を手元で比べるなら、最初のファイルの末尾へ次を足します。ここで使うURLは練習用の固定文字列で、通信は行いません。

const sampleURL = new URL("https://example.com/archive.html?q=地図#results");
console.log(readSearchState(sampleURL.search));
console.log(sampleURL.searchParams.get("q"));

一つ目はsearchと「地図」を持つ状態、二つ目は文字列の「地図」です。URLのsearchParamsから直接読む方法もあります。再確認では、末尾の#resultsが検索語に混ざっていないことを見ます。疑問符や等号で手作業の切り分けを増やす前に、渡すものがURL全体か検索部分かを確認しましょう。

任意のURLを入力してもらう仕組みに広げる場合、new URLの仕様では不正なURLに例外が発生します。そこでは例外を受け止めてURLの入力を案内する処理が別途必要です。固定文字列だけを使う今回の例を、そのまま外部入力の受付にしません。

記号を含むURLでも、案内文と検索語が合うか試す

最後は時間の目安を15分にして、完成したreadSearchStateへ次の値を渡します。URLSearchParamsが返す値は復号済みなので、getの結果をさらにdecodeURIComponentへ渡す処理は足しません。パーセント記号で表された文字を、重ねて変換しないためです。

  • ?q=%E5%9C%B0%E5%9B%B3は、searchと「地図」になる。
  • ?q=paper+mapは、searchと「paper map」になる。URL内のこのプラスは空白として読まれる。
  • ?q=C%2B%2Bは、searchと「C++」になる。プラス自体を残す例として比べる。
  • ?q=%2520は、searchと文字列「%20」になる。二重に復号して空白へ変えない。
  • ?qと?q=%20%20は、ともにemptyになる。

実行前に期待値を書き、statusとqueryの両方を照合します。searchMessageにも結果を渡し、C++なら「C++」を検索します、空白だけなら「検索語を入力してください」となることを確認します。文字の復元と表示状態が合ってから、filterによる一覧の絞り込みへ値を渡す段階を考えます。今回の関数だけでは、再読み込み後の画面表示やブラウザーの戻る操作への対応は完成しません。

表示する際の案内や入力の扱いを練習するには、JavaScriptの無料レッスンを参照できます。まずは同じ検索部分を渡せば、同じstatusとqueryを取り出せることを今回の完成条件にしましょう。

記事についてのお問い合わせ

記載内容へのご質問や訂正のご連絡は、hello@junkbranding.comまでお願いします。

← 記事一覧に戻る