こんにちは、エンジニアの id:yunagi_n です。
みなさんは JavaScript において、 URL をパースするとき、どの API を使用していますか?
もっとも簡単なのは、 URL Interface
を使用することだと思います。
今回は、その URL Interface
が、 JavaScript の実行エンジンによって挙動が異なることについて書こうと思います。
事前情報
この記事の内容は、以下のバージョンにて確認を行っています。
- macOS 12.5.1
- Google Chrome 107.0.5304.121
- Safari 15.6.1
- Firefox 107.0.1
本題
URL Interface
は、 各種ブラウザおよび Node.js 上で URL を扱うためのインターフェースです。適当な URL を渡すと、下のように各部分毎に分解してくれて、 URL を元に何かしたいときに便利なインターフェースです。
const url = new URL("https://example.com") console.log(url.protocol) // => https:
今回は、そんな URL Interface
について、ブラウザー実装毎による違いについてのお話です。
例えば、最もよく使われている Chromium 系列 (V8) の場合は、例として https://example.com/test?a=b#c
のような URL を渡すと下記のような結果を返します。
const url = new URL("https://example.com/test?a=b#c") console.dir(url) /* * URL { * hash: "#c", * host: "example.com", * hostname: "example.com", * href: "https://example.com/test?a=b#c", * origin: "https://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "https:" * search: "?a=b", * username: "" * } */
このような一般的な URL (ここではプロトコル部が HTTP および HTTPS であるものを指す) であれば、どのブラウザーでも同じ挙動をしてくれます。では、例えば FTP のセキュア版のプロトコルである FTPS を含んだ ftps://example.com/test
を渡すとどうなるでしょうか? V8 の場合は以下のような結果を返します。
const url = new URL("ftps://example.com/test") console.dir(url) /* * URL { * hash: "", * host: "", * hostname: "", * href: "ftps://example.com/test", * origin: "null", * password: "", * pathname: "//example.com/test", * port: "", * protocol: "ftps:", * search: "", * username: "" * } */
通常の URL を渡した場合と挙動に違いがありますね。ちなみに Firefox (SpiderMonkey 系) でも同じ結果を返してくれます。 では WebKit 系列 (JavaScript Core) ではどうでしょう?答えは以下のようになります。
const url = new URL("ftps://example.com/test") console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "ftps://example.com/test", * origin: "ftps://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "ftps:", * search: "", * username: "" * } */
それぞれ、 host
部の扱いが異なっているのが特徴です。 V8 は host
部は無かったものとして扱い、 pathname
にすべてを含めているのに対し、 JavaScript Core はおそらく私たちがイメージした結果と同じもの、つまりは host
部を example.com
として返してくれています。
ではセキュアではない FTP 、つまりはプロトコル部が ftp:
の URL を渡すとどうなるでしょうか?答えはすべてのブラウザーで次のような結果になります。
const url = new URL("ftp://example.com/test") console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "ftp://example.com/test", * origin: "ftp://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "ftp:" * search: "", * username: "" * } */
これから分かるように、 Chromium 系列と Firefox 系列は、プロトコル部によって、 host
および hostname
のパース結果が異なります。
では、挙動が異なるのがこれだけだというと、他にも異なる部分があります。
例えば、 hostname
に大文字小文字の両方を含む文字列を渡した際の結果は、一般的なプロトコルの場合は以下のようになります。
const url = new URL("http://ExAmple.COM/test") console.dir(url) /* * URL { * hash: "", * host: "example.com", * hostname: "example.com", * href: "http://example.com/test", * origin: "http://example.com", * password: "", * pathname: "/test", * port: "", * protocol: "http:" * search: "", * username: "" * } */
しかしそうでないもの、ここでは Web3 の文脈でよく使われている IPFS プロトコルの URL を渡した場合、 Chromium 系や Safari では以下のようになり、
const url = new URL("ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe") console.dir(url) /* * URL { hash: "", host: "", hostname: "", href: "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe", origin: "null", password: "", pathname: "//QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe", port: "", protocol: "ipfs:", search: "", username: "", * } */
SpiderMonkey 系 (Firefox) ではこうなります。
const url = new URL("ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe") console.dir(url) /* * URL { hash: "", host: "qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", hostname: "qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", href: "ipfs://QmR3u53ksjcNVzUinvC1hjjKxEpWR2a9SeWhhh7MtofjHe/", origin: "ipfs://qmr3u53ksjcnvzuinvc1hjjkxepwr2a9sewhhh7mtofjhe", password: "", pathname: "/", port: "", protocol: "ipfs:", search: "", username: "", * } */
面白いのが、 href
は大文字小文字を区別していますが、 host
などその他は大文字小文字を区別せず、すべて小文字で表されています。
結論としては、図にまとめると以下のような挙動をします。
Parse Result \ URL Protocol | http / https / ftp | ftps | ipfs |
---|---|---|---|
hash | すべてのブラウザで挙動は一致 | - | - |
host | すべてのブラウザで挙動は一致 | Safari で挙動が異なる | Firefox で挙動が異なる |
hostname | すべてのブラウザで挙動は一致 | Safari で挙動が異なる | Firefox で挙動が異なる |
href | すべてのブラウザで挙動は一致 | すべてのブラウザで挙動は一致 | Firefox で挙動が異なる |
origin | すべてのブラウザで挙動は一致 | Safari で挙動が異なる | Firefox で挙動が異なる |
pathname | すべてのブラウザで挙動は一致 | Safari で挙動が異なる | Firefox で挙動が異なる |
protocol | すべてのブラウザで挙動は一致 | すべてのブラウザで挙動は一致 | すべてのブラウザで挙動は一致 |
では、すべてのブラウザで挙動を揃えるにはどのようにすればよいのでしょうか?
答えは簡単で、 URL Interface
を使用していないパーサーライブラリを使用します。
例としては url-parse
などが使用できます。
ただし、こちらも内部で isSpecial
という関数で一部プロトコルでのみ、 origin
をセットしていたりするので、プロトコルが違えば、 origin
に限っては挙動が異なります。
しかし、それ以外についてはどのブラウザ、プロトコルでも共通の動作をしているので、より多くのブラウザで同一の挙動を実現するには、十分ではないでしょうか。
ということで、今回は URL Interface
の挙動の違いについて、解説しました。