標準の Web API で URL のパターンマッチングを処理する URLPattern
Web アプリケーションにおけるルーティングは重要な要素です。URL Pattern API は URL のパターンマッチングを標準化するための Web API であり、ブラウザやサーバーサイド環境で一貫した方法で URL パターンを処理できます。この記事では、URLPattern API の基本的な使い方とパターン構文について解説します。
Web アプリケーションにおいてルーティングは重要な要素です。URL はアプリケーションの状態を表現し、ユーザーが特定のリソースにアクセスするための手段となります。id や slug のような一意な識別子を URL に含めることによる動的なコンテキストの提供は、一般的な手法として広く採用されています。
動的なルーティングを実現するために標準的な手法は存在していませんが、Express や Hono, Next.js のような多くの Web フレームワークは path-to-regexp に基づいた構文を採用しています。フレームワークごとに多少の方言はあるものの、基本的な考え方は共通しています。
- 名前付きグループ(
/users/:id):idパラメータにマッチした部分を抽出 - ワイルドカード(
posts/*):posts/以下の任意のパスにマッチ - オプショナルセグメント(
/archive/:year/:month?):monthパラメータは省略可能 - 正規表現(
/product/:id(\\d+)):idパラメータは数字にマッチ
この構文は多くの開発者にとって馴染み深いものであり、フレームワークを変更しても学習コストが低いという利点があります。しかし、これらの構文は標準化されておらず、フレームワーク間で微妙な違いが存在するため、異なる環境での一貫性を欠くことがあります。
上記のような構文を標準化する目的で WHATWG によって提案されたのが URL Pattern API です。この標準は URL のパターンマッチングを扱うための API を定義しており、ブラウザやサーバーサイド環境で一貫した方法で URL パターンを処理できるようにします。単に構文を標準化するだけでなく、余分なライブラリを導入せずにネイティブに URL パターンマッチングを行える点が大きな利点です。
この記事では、URL Pattern API の基本的な使い方について説明します。
URLPattern の基本構文
URLPattern の構文は基本的に path-to-regexp から採用されているため、多くの開発者にとって馴染み深いものとなっています。URLPattern コンストラクタに pathname オプションを渡すことで、URL のパス部分に対するパターンを定義できます。
const pattern = new URLPattern({ pathname: "/users/:id" });すべてのパラメーターを明示的に指定すると、以下のようになります。
const pattern = new URLPattern({
protocol: "https",
username: "",
password: "",
hostname: "example.com",
port: "",
pathname: "/users/:id",
search: "*", // 未指定と同義
hash: "*", // 未指定と同義
});オブジェクトを渡す代わりに、文字列として URL パターンを指定することもできます。なお、文字列で指定する場合には : のような曖昧な文字列に注意してください。この文字列はプロトコルの区切り文字 :// とパターンの一部(例: /:id)の両方に使用されますが、URLPattern API では常にパターンの一部として解釈されます。プロトコルのサフィックスとして : を使用したい場合は about://blank のようにエスケープする必要があります。
const pattern = new URLPattern("/users/:id", "https://example.com");パターンマッチングの実行
作成した URLPattern オブジェクトを使用して、URL がパターンにマッチするかどうかを確認には以下の 2 つの方法があります。2 つの方法の違いは返される値の型のみであり、どちらも使用されるアルゴリズムは同じです。
test()メソッド: マッチした場合にtrue、マッチしなかった場合にfalseを返します。exec()メソッド: マッチした場合に詳細な情報を含むオブジェクトを返し、マッチしなかった場合はnullを返します。
以下は test() メソッドの使用例です。/users/:id パターンに対して、/users/123 はマッチし、/posts/123 はマッチしないことが確認できます。
const pattern = new URLPattern({ pathname: "/users/:id" });
console.log(pattern.test({ pathname: "/users/123" })); // true
console.log(pattern.test({ pathname: "/posts/123" })); // falseexec() メソッドを使用すると、マッチした場合にパラメーターの値を含む詳細な情報を取得できます。以下の例では、/users/123 にマッチした際に id パラメーターの値が 123 であることが確認できます。マッチしなかった場合は null が返されます。
const pattern = new URLPattern({ pathname: "/users/:id" });
const result = pattern.exec({ pathname: "/users/123" });
console.log(result.pathname.groups); // { id: "123" }
const noMatch = pattern.exec({ pathname: "/posts/123" });
console.log(noMatch); // nullパターンの構文
URLPattern のパターン構文は path-to-regexp に基づいています。いくつかの構文を確認しましょう。
固定文字列
固定文字列はそのままの文字列にマッチします。
const pattern = new URLPattern({ pathname: "/about" });
console.log(pattern.test({ pathname: "/about" })); // true
console.log(pattern.test({ pathname: "/contact" })); // false
console.log(pattern.test({ pathname: "/about/us" })); // false正規表現
正規表現を使用して、より柔軟なパターンマッチングが可能です。以下の例では、id パラメーターが数字にマッチするように指定しています。
const pattern = new URLPattern({ pathname: "/users/:id(\\d+)" });
console.log(pattern.test({ pathname: "/users/123" })); // true
console.log(pattern.test({ pathname: "/users/abc" })); // false
console.log(pattern.test({ pathname: "/posts/123" })); // false正規表現を使用する場合は必ずしも名前付きグループを使用する必要はありません(無名グループ)。以下の例では、数字にマッチする部分を抽出しています。
const pattern = new URLPattern({ pathname: "/items/(\\d+)" });
const result = pattern.exec({ pathname: "/items/456" });
console.log(result.pathname.groups); // { "0": "456" }グループ修飾子
グループ修飾子を使用して、パターンの繰り返しやオプショナル性を指定できます。以下の 3 つの修飾子が利用可能です。
?: 直前のセグメントが 0 回または 1 回出現することを示す(オプショナルセグメント)*: 直前のセグメントが 0 回以上出現することを示す(ワイルドカード)+: 直前のセグメントが 1 回以上出現することを示す
以下の例では month パラメーターがオプショナルであることを示しています。/month セグメントが存在してもしなくてもマッチします。
const pattern = new URLPattern({ pathname: "/archive/:year/:month?" });
console.log(pattern.test({ pathname: "/archive/2023" })); // true
console.log(pattern.test({ pathname: "/archive/2023/11" })); // true
console.log(pattern.test({ pathname: "/archive/2023/11/extra" })); // false* 修飾子を使用すると、任意の数のセグメントにマッチできます。以下の例では、posts/ 以下の任意のパスにマッチします。
const pattern = new URLPattern({ pathname: "/posts/:slug*" });
console.log(pattern.test({ pathname: "/posts/123" })); // true
console.log(pattern.test({ pathname: "/posts/2023/11/my-post" })); // true
console.log(pattern.test({ pathname: "/posts/" })); // true
console.log(pattern.test({ pathname: "/users/123" })); // false
const result = pattern.exec({ pathname: "/posts/2023/11/my-post" });
console.log(result.pathname.groups); // { slug: "2023/11/my-post" }+ 修飾子を使用すると、少なくとも 1 回の出現が必要であることを示せます。以下の例では、tag パラメーターが少なくとも 1 つ以上存在する必要があります。
const pattern = new URLPattern({ pathname: "/articles/:tag+" });
console.log(pattern.test({ pathname: "/articles/tech" })); // true
console.log(pattern.test({ pathname: "/articles/tech/javascript" })); // true
console.log(pattern.test({ pathname: "/articles/" })); // falseワイルドカード
ワイルドカードは * を使用して、任意の文字列にマッチさせることができます。ワイルドカードは貪欲にマッチし、可能な限り多くの文字列をキャプチャします。
const pattern = new URLPattern({ pathname: "/files/*" });
console.log(pattern.test({ pathname: "/files/document.pdf" })); // true
console.log(pattern.test({ pathname: "/files/images/photo.jpg" })); // true
console.log(pattern.test({ pathname: "/files/" })); // true
console.log(pattern.test({ pathname: "/users/123" })); // falseワイルドカードは中間セグメントにも使用できます。
const pattern = new URLPattern({ pathname: "/blog/*/comments" });
console.log(pattern.test({ pathname: "/blog/my-first-post/comments" })); // true
console.log(pattern.test({ pathname: "/blog/2023/11/my-post/comments" })); // true
console.log(pattern.test({ pathname: "/blog/comments" })); // falseグループ区切り文字
グループ区切り文字は {} で囲まれた部分を示します。グループ区切り文字は結果にキャプチャされませんが、パターンの一部として機能します。例えば book{s}? は book または books にマッチしますが、結果には s は含まれません。
const pattern = new URLPattern({ pathname: "/book{s}?" });
console.log(pattern.test({ pathname: "/book" })); // true
console.log(pattern.test({ pathname: "/books" })); // true
console.log(pattern.test({ pathname: "/bookss" })); // false
const result = pattern.exec({ pathname: "/books" });
console.log(result.pathname.groups); // {}検索・ハッシュ部分のパターンマッチング
URLPattern は URL の検索部分(クエリパラメーター)やハッシュ部分にもパターンマッチングを適用できます。検索部分とハッシュ部分のパターンは search および hash プロパティで指定します。
const pattern = new URLPattern({
pathname: "/search",
search: "?q=:query&lang=:lang?",
});
console.log(
pattern.test({ pathname: "/search", search: "?q=javascript&lang=en" }),
); // true
const result = pattern.exec({
pathname: "/search",
search: "?q=javascript&lang=en",
});
console.log(result.search.groups); // { query: "javascript", lang: "en" }const pattern = new URLPattern({
pathname: "/page",
hash: "#section=:section",
});
console.log(pattern.test({ pathname: "/page", hash: "#section=2" })); // true
const result = pattern.exec({ pathname: "/page", hash: "#section=2" });
console.log(result.hash.groups); // { section: "2" }パターンの正規化
URL がパターンにマッチするかどうかを評価する際、URLPattern は自動で URL を正規化します。これには以下のような処理が含まれます。
pathnameプロパティにおける Unicode 文字のパーセンテージエンコーディングhostnameプロパティにおける Punycode エンコーディング- デフォルトポートの削除(例:
httpの場合はポート 80、httpsの場合はポート 443) /foo/./barのようなパスは/foo/barに正規化
例えば /ユーザー/123 というパスは、Unicode 文字がパーセンテージエンコーディングされた /%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC/123 として正規化され、どちらの形式でもパターンにマッチします。
const pattern = new URLPattern({ pathname: "/ユーザー/:id" });
console.log(pattern.test({ pathname: "/ユーザー/123" })); // true
console.log(
pattern.test({ pathname: "/%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC/123" }),
); // trueパターンの大文字・小文字の区別
デフォルトでは URLPattern は大文字・小文字を区別します。例えば /users/:id パターンは /Users/123 にはマッチしません。
const pattern = new URLPattern({ pathname: "/users/:id" });
console.log(pattern.test({ pathname: "/users/123" })); // true
console.log(pattern.test({ pathname: "/Users/123" })); // falseURLPattern コンストラクタのオプションで ignoreCase を true に設定すると、大文字・小文字を区別しないマッチングが可能になります。
const pattern = new URLPattern(
{ pathname: "/users/:id" },
{ ignoreCase: true },
);
console.log(pattern.test({ pathname: "/users/123" })); // true
console.log(pattern.test({ pathname: "/Users/123" })); // trueまとめ
- URLPattern API は URL のパターンマッチングを標準化するための Web API
URLPatternコンストラクタを使用して URL パターンを定義し、test()およびexec()メソッドでマッチングを実行できる- path-to-regexp に基づいた構文を採用しており、名前付きグループ、正規表現、グループ修飾子、ワイルドカードなどが利用できる
- 検索部分やハッシュ部分にもパターンマッチングを適用できる
- URL の正規化が自動で行われ、大文字・小文字の区別もオプションで設定できる
