【JavaScript】express ライクな URL ルーティング URLPattern

[2022-10-26 更新]

URL ルーティングといえば主にサーバーサイドの話ですが、node の express といったフレームワークではおなじみでしょう。この express のような URL ルーティングをブラウザーで動作する JavaScript の API として提供するのが URLPattern です。

node の express といったサーバーサイドに詳しくない方には分かりにくいですよね。URLPattern を使うと、とても分かりやすいコードで、URL が期待通りのパターンかどうかを評価したり、URL を分解して特定の部位を取り出すことができるようになります。

今回は、URLPattern が生まれた経緯から使い方まで紹介します。

URLPattern とは

まずは実際のコードを見てみましょう。次のコードはまず URL のパターンを定義するために URLPattern オブジェクトを生成します。そして、その URLPattern オブジェクトの test() メソッドを使って、引数の URL がパターンに一致しているかを評価しています。

// URLPattern オブジェクトを生成
const pattern = new URLPattern({
  pathname: '/post/:pno',
});

// パターンに一致しているかだけを評価する
if (pattern.test('https://example.jp/post/123')) {
  console.log('Hit!');
}

URLPattern オブジェクトを生成するときに指定した pathname に注目してください。'/post/:pno' の部分が express のルーティングの書き方とそっくりですね。:pno の部分に何か文字列が当てはまることになります。言い換えると変数みたいなものですね。そして test() メソッドは、引数に指定した URL がパターンに一致すれば true を、そうでなければ false を返します。

:pno に相当する部分が何だったのかを知ることも可能です。そのためには exec() メソッドを使います。

// パターンからパーツを取り出す
const result = pattern.exec('https://example.jp/post/123');
if (result) {
  console.log(JSON.stringify(result, null, '  '));
}

この結果は次の通りです。

{
  "hash": {
    "groups": {
      "0": ""
    },
    "input": ""
  },
  "hostname": {
    "groups": {
      "0": "example.jp"
    },
    "input": "example.jp"
  },
  "inputs": [
    "https://example.jp/post/123"
  ],
  "password": {
    "groups": {
      "0": ""
    },
    "input": ""
  },
  "pathname": {
    "groups": {
      "pno": "123"
    },
    "input": "/post/123"
  },
  "port": {
    "groups": {
      "0": ""
    },
    "input": ""
  },
  "protocol": {
    "groups": {
      "0": "https"
    },
    "input": "https"
  },
  "search": {
    "groups": {
      "0": ""
    },
    "input": ""
  },
  "username": {
    "groups": {
      "0": ""
    },
    "input": ""
  }
}

ご覧の通り、exec() メソッドは、引数に指定した URL をパーツに分離してくれます。:pno に相当する文字列を抜き出すと、次のようなコードになります。

const result = pattern.exec('https://example.jp/post/123');
if (result) {
  const pno = result.pathname.groups.pno;
  console.log(pno); // "123"
}

以上で使い方はなんとなくお分かりいただけたのではないでしょうか。これまで正規表現を駆使して URL を評価しなければいけませんでしたが、簡単な表記で URL が評価できるようになった点が便利ですね。

URLPattern が生まれた理由

URLPattern はもともとは Service Worker での必要性から提案されたようです。そのあたりの詳細は「Service Worker Scope Pattern Matching Explainer」というドキュメントで説明されています。以下、筆者の解釈ですが、かなり短めに簡単に解説します(前述のドキュメントの内容とは異なります)。

Service Worker を登録する際にスコープを指定することが可能です。

navigator.serviceWorker.register("/sw.js", {
  scope: "/product/1/"
});

この場合、本来であれば sw.js が存在する / 配下のリクエストがこの Service Worker の対象になりますが、scope を指定しているため、/product/1/ 配下のリクエストのみが Service Worker の対象になります。

このスコープを柔軟に定義できるようにしたいことから、URLPattern が提案されたようです。

navigator.serviceWorker.register("/sw.js", {
  scope: new URLPattern({
    baseURL: self.location,
    pathname: '/product/:pno/'
  })
});

これなら、/product/1/ だけでなく、/product/2//product/3/ も対象にすることができます。

標準化

URLPattern は W3C の Web Platform Incubator Community Group にて提案された API です。提案の詳細は GitHub で公開されています。

少なくともまだ W3C 標準のフォーマットでの仕様書も見当たりませんので、かなり初期段階の API のようです。個人的には、こんな状態でブラウザーに実装しちゃっても良いのかと心配になります。

場合によっては、いつか部分的に仕様が変更されてしまうかもしれませんね。その場合、実装はどうなるのでしょうか…

パターンの作り方

前述のサンプルコードでは pathname だけを評価しましたが、URLPattern は URL のあらゆるパーツを評価できます。フルで指定すると次のようになります。

const pattern = new URLPattern({
  protocol: 'https',
  username: '',
  password: '',
  hostname: 'example.jp',
  port: '',
  pathname: '/post/:pno',
  search: '*',
  hash: '*',
});

空文字列は URL に存在しないことを意味し、*(アスタリスク)は何でも OK という意味です。そのため、以下のいずれの URL もこのパターンに一致します。

https://example.jp/post/123
https://example.jp/post/123?foo=bar#chaper2

しかし、次の URL はパターンに一致しません。

https://example.jp:10443/post/123
https://example.jp/post/?foo=bar#chapter2

一つ目はポート番号が異なるので NG です。パターンの指定では port は空文字列ですが、https の標準ポート番号なら許されます。しかし、それ以外のポート番号が指定されると NG になります。

二つ目は pno に相当する部分がないため NG です。

日本語のパス

このブログもそうですが、URL のパスに日本語を使うサイトもあります。もちろん、URLPattern は日本語にも対応しています。

const pattern = new URLPattern({
  pathname: '/:title/',
});
const result = pattern.exec('https://webfrontend.ninja/画面の色を抜き出すeye-dropper-api/');
console.log(result.pathname.groups.title);

このコードは次のような結果を出力します。

%E7%94%BB%E9%9D%A2%E3%81%AE%E8%89%B2%E3%82%92%E6%8A%9C%E3%81%8D%E5%87%BA%E3%81%99eye-dropper-api

このように、日本語のままではなく URL エンコードされた文字列を返します。

正規表現

URLPattern のパターンの定義では正規表現も使うことができます。次の例では pno の値を数値のみに限定しています。

const pattern = new URLPattern({
  pathname: '/post/:pno(\\d+)/',
});
const result = pattern.exec('https://example.jp/post/123/');
console.log(result.pathname.groups.pno); // "123"

大文字・小文字を無視

パターンの評価で大文字・小文字を無視することもできます。

const input = { pathname: '/post/:pno(\\d+)/' };
const options = { ignoreCase: true };
const pattern = new URLPattern(input, options);
const result = pattern.exec('https://example.jp/POST/123/');
console.log(result.pathname.groups.pno); // "123"

URLPattern コンストラクタには第二引数にオプションをオブジェクトとして指定することができます。現在、指定可能なオプションは ignoreCase のみです。このプロパティの型は Boolean で、デフォルト値は false です。ここに true を指定することで、URL を評価する際に、大文字・小文字を無視するようになります。

上記サンプルコードでは patuname のパターンを小文字で指定しています。一方、exec() メソッドでは “POST” の部分が大文字になっています。大文字・小文字が一致しませんが、ignoreCasetrue をセットしているため、一致したものとして処理されます。

なお、このオプションは Chrome 107 以降でサポートされています。

まとめ

URLPattern は、2022 年 10 月現在、Chrome や Edge などの Chromium ベースのブラウザーでサポートされています。

これまで URL ルーティングは正規表現を駆使してコードを書く必要があり、正規表現の特性上、コードが分かりづらいというのが難点でした。URLPattern を使えば、高度な正規表現を使う必要性も減り、誰もが分かりやすいコードになるのではないでしょうか。

標準化が初期段階のため、今後の動向が気になります。他のブラウザーにも早く実装され、事実上の標準になってしまうことを祈りたいですね。

今回は以上で終わりです。最後まで読んでくださりありがとうございました。それでは次回の記事までごきげんよう。

Share