JavaScript

JavaScriptのURLSearchParams.sort():クエリのキー順をそろえる

この記事でわかること

URLSearchParams.sort()でクエリのキー順をそろえる方法を解説。引数・戻り値、ブラウザーとNode.jsの実行手順、同名キーの順序維持、元データの変更や再エンコードの注意点を学べます。

目的:検索条件の並び順による文字列の違いを減らす

検索画面で作るpage=2&category=booksとcategory=books&page=2は、アプリケーションによっては同じ検索条件を表します。しかし、文字列として比較すると一致しません。検索結果のキャッシュキーやテストの期待値を作る際、条件を追加した順番によって結果が変わると扱いにくくなります。

URLSearchParams.sort()は、クエリのキーと値の組をキー順に並べ替えるメソッドです。値を手作業で分解して並べ直す必要がなく、同じキーが複数あるデータも保持できます。

この記事では、キーの順番をそろえたクエリ文字列を作る処理に絞って解説します。URL全体の同一性を判定したり、検索条件の意味を検証したりする処理は別に必要です。

動作環境と対応バージョン

ブラウザーとNode.jsの両方で利用できます。ブラウザー専用の画面操作は使わないため、この記事のJavaScriptコードは両環境で共通です。

  • ブラウザーでは、現行のChrome、Edge、Firefox、Safariを対象にします。MDNは、2018年4月以降、主要ブラウザーを横断して利用できる機能としています。
  • Node.jsでは、本文の実行環境として22系以降を想定します。外部パッケージやビルド処理は不要です。
  • Node.js公式の履歴では、メソッド自体の追加はv7.7.0およびv6.13.0です。ただし、インポートせず使うグローバルのURLSearchParamsはv10.0.0からです。

機能が追加された古いバージョンを、新しい開発環境として選ぶ必要はありません。導入履歴と実際に使う環境を分けて判断してください。対応状況はMDNのsort()、Node.jsの履歴は公式URL APIドキュメントで確認できます。

引数・戻り値と並べ替えの規則

呼び出し方はparams.sort()です。引数はありません。比較関数や降順指定を受け取るAPIではありません。

戻り値はundefinedです。新しいオブジェクトを返すのではなく、呼び出したURLSearchParams自身を書き換えます。文字列が必要なら、その後にparams.toString()を呼びます。

キー順と安定ソートを理解する

並べ替えはキーのUTF-16コード単位の値に従います。日本語の読み順や、人間が考える自然な数値順とは限りません。たとえば、キーがitem10とitem2なら、数字部分を数値として比較する処理は行われません。

また、同じキーの組どうしは、もとの相対順序を保ちます。これが安定ソートです。tag=z&tag=aは、並べ替え後もtag=z&tag=aの順です。値まで昇順になると考えないようにしてください。これらの規則と戻り値はMDNのメソッド仕様に記載されています。

自己完結したコードで実行する

次のコードはクエリ文字列を受け取り、そのキー順をそろえた文字列を返します。ネットワーク通信やファイルの読み書きは行いません。

JavaScript
(() => {
  "use strict";

  function sortQuery(query) {
    if (typeof query !== "string") {
      throw new TypeError("queryは文字列で指定してください");
    }

    const params = new URLSearchParams(query);
    params.sort();
    return params.toString();
  }

  const first = "tag=z&page=2&tag=a&category=books";
  const second = "category=books&tag=z&tag=a&page=2";

  const firstKey = sortQuery(first);
  const secondKey = sortQuery(second);

  console.log("整列後: " + firstKey);
  console.log("一致: " + (firstKey === secondKey));
  console.log("元の文字列: " + first);
  console.log("空文字列: " + JSON.stringify(sortQuery("")));

  try {
    sortQuery(null);
  } catch (error) {
    console.log(error.name + ": " + error.message);
  }
})();

sortQueryの引数queryは、URL全体ではなくクエリ部分の文字列です。戻り値は、先頭の?を含まない文字列になります。関数内で新しいURLSearchParamsを作るため、呼び出し元の文字列は変わりません。

ブラウザーでの実行方法

通常のWebページを開き、開発者ツールのConsoleにコード全体を貼り付けて実行します。ページ内の要素には依存しません。コードを即時実行関数で囲んでいるので、同じConsoleで繰り返し実行しても、トップレベルの定数名が衝突しにくい形です。

Node.jsでの実行方法

コード全体をUTF-8のquery-sort.jsとして保存します。ターミナルでそのファイルがあるディレクトリーに移動し、次を実行してください。

Shell
node --version
node query-sort.js

最初のコマンドで利用中のバージョンを確認できます。コードにはimportやrequireがなく、追加のパッケージ設定も不要です。

期待される出力

ブラウザーとNode.jsで、コードが出力する内容は次のとおりです。Consoleには別途、評価結果のundefinedが表示される場合があります。

CODE
整列後: category=books&page=2&tag=z&tag=a
一致: true
元の文字列: tag=z&page=2&tag=a&category=books
空文字列: ""
TypeError: queryは文字列で指定してください

同名のtagは、値がz、aの順を保ちます。この例は同期処理なので、awaitやPromiseの拒否処理は必要ありません。入力の型が違う場合には関数側で例外を投げ、例ではtry...catchで受け取っています。出力は仕様に基づく期待値であり、実機での検証結果ではありません。

失敗例と制約

戻り値をつなげて呼び出さない

params.sort().toString()と書くと、undefinedに対してメソッドを呼び出すためTypeErrorになります。次のコードでは、その失敗を例外として確認できます。

JavaScript
(() => {
  const params = new URLSearchParams("b=2&a=1");

  try {
    params.sort().toString();
  } catch (error) {
    console.log(error.name);
  }

  console.log(params.toString());
})();

期待出力は次のとおりです。後続の呼び出しは失敗しても、sort()による変更はすでに終わっています。

CODE
TypeError
a=1&b=2

既存のURLSearchParamsを変更したくない場合は、new URLSearchParams(original)でコピーしてから並べ替えます。

URL全体をクエリとして渡さない

new URLSearchParams("https://example.com/?b=2&a=1")は、URL全体を解析する書き方ではありません。URL文字列なら、まずnew URL(input)で解析してクエリ部分を取り出します。相対URLを解析する場合には基準URLも必要です。コンストラクターの扱いはMDNのURLSearchParams解説で確認できます。

並び替えだけでもエンコード表記が変わる

URLのsearchParamsは、そのURLと連動しています。次の例ではキーが1つしかありませんが、sort()後にクエリの表記が変わります。

JavaScript
(() => {
  const url = new URL("https://example.com/?q=hello%20world~");
  console.log(url.search);
  url.searchParams.sort();
  console.log(url.search);
})();

期待出力は次のとおりです。

CODE
?q=hello%20world~
?q=hello+world%7E

これは、URLSearchParamsのシリアライズで空白が+、チルダが%7Eになるためです。署名付きURLなど、元の文字列そのものが重要なデータには、仕様を確認せず適用しないでください。この挙動はMDNのsearchParamsとsearchの相互作用にも例があります。

活用事例と導入時の判断

アプリケーション内の検索結果キャッシュでは、キーを作る直前にクエリを整列すると、条件を追加した順番による文字列の違いを減らせます。Node.js公式も、キャッシュヒットを増やす用途を挙げています。Node.jsのsort()解説

ただし、クエリだけをキャッシュキーにすると、異なるパスの結果が衝突する可能性があります。実際には対象のパスや、結果に影響する利用者・言語などもキー設計に含めてください。これはアプリケーション側で判断する設計事項です。

テストでは、キーの順番だけが異なる入力を比較する前処理に使えます。一方、同名キーの値順が異なる入力は、整列後も一致するとは限りません。値順を無視してよいかは、APIの意味を確認する必要があります。

導入するなら、まず検索条件を作る処理の出口1か所に適用し、重複キーと空白を含むケースを確認するのが現実的です。すべてのURLを一律に変更するより、同一視してよい範囲を決めることで、意図した比較やキャッシュ設計につなげられます。

公式参考資料

PREFERRED SOURCES

Googleの優先するニュース提供元にSEの部屋を追加

Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。

Googleの設定画面で確認する 新しいタブで開きます

候補にSEの部屋が表示されない場合は、まだ追加できません。

y.
WRITTEN BY

y_ymo10

SEの部屋で、HTML・CSSからJavaScript・React・Next.jsまで、Web制作の開発ノートを公開しています。

ほかの開発ノートを読む →