URLを文字列操作でパースするのはもうやめよう
URL APIが登場する前は、みんなsplit('?')や正規表現、そして祈りを駆使してURLを切り刻んでいました。値に&や=、スペース、非ASCII文字が混ざらない限りは、それでもなんとか動いていたんです。でも一度そういう文字が入れば、途端に壊れる。ブラウザにもNode.jsにも、ちゃんとしたパーサーが最初から用意されています。素直にそれを使いましょう。
呼び出し一発で、URL の各パーツがすでにバラされて、しかもデコードまで済んだ状態で手に入ります。不正な URL を渡すとコンストラクタは TypeError を投げますが、これはむしろ望ましい挙動です。おかしな URL は黙って通すよりも、その場で派手に落ちてくれたほうが、後続の処理でゴミを垂れ流すより安全ですよね。
クエリパラメータを取得する
URL オブジェクトには必ず .searchParams プロパティがあり、これが URLSearchParams オブジェクトです。クエリ文字列の読み書きはすべてこいつに任せられます。
押さえておきたいポイントがいくつかあります。
- 値はデコード済みで返ってきます。
?name=Ada%20Lovelaceなら"Ada Lovelace"が取得できます。 - すべて文字列として扱われます。
"2"は2ではありません。数値として使いたい場合はNumber()で変換しましょう。 - 同じキーが複数あっても問題ありません。
getは最初にマッチしたものだけを返し、getAllはすべての値を配列で返します。 - 存在しないキーは
undefinedではなくnullを返します。なので?? "default"との組み合わせが便利です。
クエリ文字列を組み立てる
URLSearchParams を使えば、クエリ文字列をゼロから組み立てられます。エスケープを手動でやる必要も、& で自分でつなぐ必要もありません。
オブジェクトから生成することもできます。[キー, 値] のペアを返すイテラブルならなんでも使えますし、普通のオブジェクトでもOKです。
set と append の違い: set は既存の値を上書きします。append は別の値を追加します。同じキーが複数回登場し得る場合(タグやフィルタなど)は append、単一の値しか持たないパラメータには set を使い分けましょう。
URL を書き換える
URL はライブオブジェクトなので、searchParams をいじれば .search や .href も自動的に更新されます。
既存のURLにクエリパラメータを追加するなら、これが一番スマートな書き方です。「URLにすでに?が付いてるかな?」とチェックしたり、区切り文字として&と?のどちらを使うか悩む必要もありません。
URLの他の部分も、同じ要領で書き換えられます。
パラメータをループで取り出す
URLSearchParams はイテラブルなので、for...of で回すと [キー, 値] のペアが順番に取れます。配列などと同じように keys()、values()、entries() といったヘルパーも用意されています。
キーの重複はそのまま保持される点に注目してください。tag = web の次に tag = beginner が別エントリとして出てきます。これはクエリ文字列の実際の中身に忠実な挙動です。
デバッグ用にサクッとプレーンなオブジェクトで中身を確認したいときは Object.fromEntries が便利です。ただし重複キーは潰れてしまい、最後の値だけが残る点には注意してください。
デバッグ目的なら問題ありませんが、同じキーが複数回登場しうる場合は正しく動作しません。
相対URLにはベースが必要
new URL("/search?q=js") を単独で呼ぶとエラーになります。相対パスだけでは有効なURLにならないからです。第2引数にベースURLを渡しましょう。
この解決ルールは、ブラウザが <a href> を解釈するときとまったく同じです。先頭が / ならホストからの絶対パス、スラッシュなしなら現在のパスからの相対、.. は 1 階層上に上がります。設定値のベース URL から API の URL を組み立てるときにかなり重宝します。
ブラウザ上では、window.location.href がそのまま現在ページの URL を解析するためのベースとして使えます。
const u = new URL(window.location.href);
const page = u.searchParams.get("page") ?? "1";
不正な URL を扱う
URL コンストラクタは、フォーマットが崩れた入力を渡すと例外を投げます。これ自体はありがたい仕様なのですが、ユーザーが入力した文字列や外部システムから受け取った値をパースするときは、try/catch で囲む必要があります。
モダンな実行環境では URL.canParse(input) も使えます。これは真偽値を返すチェック用のメソッドで、URL が有効かどうか確かめたいだけなら try/catch でわざわざ囲む必要がありません。
ちょっとした実用サンプル
ここまでの内容をまとめて、URL から現在のフィルタを読み取り、値を書き換えて、遷移先となる新しい URL を組み立ててみましょう。
null を渡すとそのパラメータが削除されます。それ以外の値を渡すと、設定もしくは上書きになります。フィルター UI やページネーション、ディープリンクを作るときには、形は違えど結局このパターンを書くことになります。
まとめ
new URL(string)は URL を意味のあるパーツに分解してくれます。不正な文字列を渡すと例外になります。url.searchParamsはURLSearchParamsなので、get、getAll、set、append、delete、hasをそのまま使えます。- エンコードは自動で処理されます。自分で文字列を組み立てているとき以外、
encodeURIComponentの出番はありません。 - 相対パスを解決したいときは、第2引数にベース URL を渡しましょう。
- 信頼できない入力のバリデーションには
URL.canParse(またはtry/catch)が便利です。
.split('?') で URL を分割したくなったときや、正規表現でクエリパラメータを抜き出したくなったときは、代わりにこれらの API を使ってください。コードは短く、挙動は正確で、しかもランタイムに最初から入っています。
よくある質問
JavaScriptでURLをパースするには?
文字列をURLコンストラクタに渡すだけです。const u = new URL('https://example.com/path?x=1')のように書くと、protocol、host、pathname、search、hash、そしてsearchParamsといったプロパティにアクセスできます。不正なURLを渡すと例外を投げるので、外部から受け取った文字列をパースする場合はtry/catchで囲んでおくと安心です。
クエリ文字列のパラメータを取得するには?
url.searchParams.get('name')を使います。デコード済みの値が返ってきて、該当するパラメータが無ければnullになります。?tag=a&tag=bのように同じキーが複数回出てくるケースでは、searchParams.getAll('tag')で配列としてまとめて取得できます。
URLとURLSearchParamsの違いは?
URLはプロトコル、ホスト、パス、クエリ、ハッシュといったURL全体を扱うオブジェクトです。一方、URLSearchParamsはクエリ文字列の部分だけを担当し、a=1&b=2のような文字列を単独で組み立てたり解析したりできます。URLインスタンスには.searchParamsプロパティが用意されていて、これがそのURLに紐づくURLSearchParamsとして機能します。
クエリパラメータを自分でエンコードする必要はある?
基本的には不要です。URLSearchParamsでset、appendを呼んだり、文字列として取り出したりするときに、キーと値は自動でエンコードされます。スペース、&、=、Unicode文字もきちんと処理してくれます。encodeURIComponentを自分で呼ぶのは、どうしても文字列を手組みする場合だけで、普段はそもそも手組みしないのが正解です。