Node.js 8.0で正式実装されたWHATWG URL APIを紹介!
こんにちは。
開発チームのワイルド担当、まんだいです。
少し前の話になりますが、2017年5月30日にNode.jsの8.0.0が公開されました。
このバージョンから、npmのバージョン5.0.0がバンドルされるようになり、キャッシュ周りのコードが書き直され、高速化されているようです。
過去のバージョンとの速度比較を行ったツイートも公開され、この例では、従来の1/5の速度でインストールが完了しているようです。
With #npm5 about to come out, I thought I'd update those benchmarks.
Here's the npm5 code I'm working on, vs [email protected] on a popular repo pic.twitter.com/KWPfbpE46p
— ✨11x gayer Kat✨ (@maybekatz) 2017年5月19日
現在のバージョンのV8エンジンのバージョンは5.8になりますが、V8 5.9やV8 6.0との互換性があるようで、今後のバージョンではV8エンジンのバージョンアップで更なる高速化が期待できそうとの事。 → Node.js 8.0が公開。npm 5.0バンドル、Node.js API搭載、WHATWG URLパーサーを正式サポートなど - Publickey
今回は、Node.js 8.0で正式実装された、WHATWG URL APIを見ていきたいと思います。
WHATWG URL API ってなんだ!?
実はNode.js 7系から存在したWHATWG URL APIですが、8.0.0で正式版となりました。
既に利用された事がある方も多くおられるでしょうが、「Experimental」(実験的)な立ち位置でしたので、本番環境で利用するのには、若干の躊躇があったかも知れません。
このAPIは、URLのパースを標準化するためのもので、従来のurlモジュールを拡張する形で提供されています。
const URL = require('url').URL; const beyondUrl = new URL('http://www.beyondjapan.com/?abc=123&xyz=999#first'); console.log(beyondUrl); // 結果 URL { href: 'http://www.beyondjapan.com/?abc=123&xyz=999#first', origin: 'http://www.beyondjapan.com', protocol: 'http:', username: '', password: '', host: 'www.beyondjapan.com', hostname: 'www.beyondjapan.com', port: '', pathname: '/', search: '?abc=123&xyz=999', searchParams: URLSearchParams { 'abc' => '123', 'xyz' => '999' }, hash: '#first' }
urlモジュールを使う事ももちろんできて、今まで通りの使用感です。
const url = require('url'); const beyondUrl = 'http://www.beyondjapan.com/?abc=123&xyz=999#first'; console.log(url.parse(beyondUrl)); // 結果 Url { protocol: 'http:', slashes: true, auth: null, host: 'www.beyondjapan.com', port: null, hostname: 'www.beyondjapan.com', hash: '#first', search: '?abc=123&xyz=999', query: 'abc=123&xyz=999', pathname: '/', path: '/?abc=123&xyz=999', href: 'http://www.beyondjapan.com/?abc=123&xyz=999#first' }
オブジェクト名が若干違うだけというのが、紛らわしいですが、出力されるオブジェクトの内容も少し違いますね。
WHATWG URL APIからのレスポンスでは、searchParamsというキーでクエリストリングがパースされて返ってくるのは便利です。
これだけでも使いたくなりますね。
URLオブジェクトの挙動
WHATWG APIから返ってきたURLオブジェクトは、各データにアクセスする事もできます。
const u = 'http://www.beyondjapan.com/?abc=123&xyz=999#first'; const URL = require('url').URL; const beyondUrl = new URL(u); console.log(beyondUrl.hostname); // 結果 www.beyondjapan.com
ホスト名を別のものにしてみます。
const u = 'http://www.beyondjapan.com/?abc=123&xyz=999#first'; const URL = require('url').URL; const beyondUrl = new URL(u); beyondUrl.hostname = 'example.com'; console.log(beyondUrl); // 結果 URL { href: 'http://example.com/?abc=123&xyz=999#first', origin: 'http://example.com', protocol: 'http:', username: '', password: '', host: 'example.com', hostname: 'example.com', port: '', pathname: '/', search: '?abc=123&xyz=999', searchParams: URLSearchParams { 'abc' => '123', 'xyz' => '999' }, hash: '#first' }
ホスト名は、ホスト名だけを認識して書き換えるので、以下のようにした場合もホスト名だけ変更されます。
const u = 'http://www.beyondjapan.com/?abc=123&xyz=999#first'; const URL = require('url').URL; const beyondUrl = new URL(u); beyondUrl.hostname = 'example.com:443'; // ポート番号も付けてみる console.log(beyondUrl); // 結果 URL { href: 'http://example.com/?abc=123&xyz=999#first', origin: 'http://example.com', protocol: 'http:', // 変わらない username: '', password: '', host: 'example.com', hostname: 'example.com', port: '', // 変わらない pathname: '/', search: '?abc=123&xyz=999', searchParams: URLSearchParams { 'abc' => '123', 'xyz' => '999' }, hash: '#first' }
ポート番号を変えたいなら、ポート番号をちゃんと変更する必要があります。
const u = 'http://www.beyondjapan.com/?abc=123&xyz=999#first'; const URL = require('url').URL; const beyondUrl = new URL(u); beyondUrl.port = 443; console.log(beyondUrl); // 結果 URL { href: 'http://www.beyondjapan.com:443/?abc=123&xyz=999#first', origin: 'http://www.beyondjapan.com:443', protocol: 'http:', // 変わらない username: '', password: '', host: 'www.beyondjapan.com:443', hostname: 'www.beyondjapan.com', port: '443', pathname: '/', search: '?abc=123&xyz=999', searchParams: URLSearchParams { 'abc' => '123', 'xyz' => '999' }, hash: '#first' }
しかしながら、hostを変更した場合はそうではないようです。
const u = 'http://www.beyondjapan.com/?abc=123&xyz=999#first'; const URL = require('url').URL; const beyondUrl = new URL(u); beyondUrl.host = 'example.com:443'; console.log(beyondUrl); // 結果 URL { href: 'http://example.com:443/?abc=123&xyz=999#first', origin: 'http://example.com:443', protocol: 'http:', // 変わらない username: '', password: '', host: 'example.com:443', // 変わった hostname: 'example.com', // 変わった port: '443', // 変わった pathname: '/', search: '?abc=123&xyz=999', searchParams: URLSearchParams { 'abc' => '123', 'xyz' => '999' }, hash: '#first' }
URLSearchParamsクラス
先ほどはURLオブジェクトの挙動を見てみましたが、今度はURL.searchParamsから得られるURLSearchParamsクラスを見てみます。
このオブジェクトはNode.js 7系で実装されたクラスで、クエリストリングをパースし、getter/setterを提供するクラスになります。
公式ドキュメントにもquerystringモジュールと比較されているところがありますが、URLSearchParamsクラスはquerystringモジュールほど小回りが効くものではないとして、querystringモジュールが不要になるという訳ではないようです。
URLSearchParamsクラスは、urlモジュールの1クラスとして提供されているので、単独で使う事も可能です。
よって、解析だけでなく、生成にも利用できるパワフルなクラスです。
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const qsObject = { abc:123, xyz:456, aaa:789 }; const qsIterable = [ ['abc', 123], ['xyz', 456], ['aaa', 789], ]; const qsMap = new Map(); qsMap.set('abc', 123); qsMap.set('xyz', 456); qsMap.set('aaa', 789); function* qsGenerator(){ yield ['abc', 123]; yield ['xyz', 456]; yield ['aaa', 789]; } const params1 = new URLSearchParams(qs); // 普通のクエリストリング形式の文字列でも const params2 = new URLSearchParams(qsObject); // 普通のオブジェクトでも const params3 = new URLSearchParams(qsIterable); // イテレータでも const params4 = new URLSearchParams(qsMap); // Mapオブジェクトでも const params5 = new URLSearchParams(qsGenerator()); // ジェネレータでも console.log(params1); console.log(params2); console.log(params3); console.log(params4); console.log(params5); // 結果 URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' } URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' } URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' } URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' } URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' }
Object、配列、Mapオブジェクト、ジェネレータなど何でも食べる好き嫌いのない子という感じです。
作成されたURLSearchParamsオブジェクトは、色々なメソッドを持っています。
追加するappend
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); params.append('bbb', 963); console.log(params.toString()) // 結果 // abc=123&xyz=456&aaa=789&bbb=963
削除するdelete
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); params.delete('bbb'); console.log(params.toString()); // 結果 // abc=123&xyz=456&aaa=789
イテレータを返すentries
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); for (let v of params.entries()) console.log(v); // 結果 /* [ 'abc', '123' ] [ 'xyz', '456' ] [ 'aaa', '789' ] */
全件ループのforEach
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); params.forEach((value, key, p) => { console.log(value, key, p); }) // 結果 /* 123 abc URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' } 456 xyz URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' } 789 aaa URLSearchParams { 'abc' => '123', 'xyz' => '456', 'aaa' => '789' } */
引数のkeyのvalueを返すget
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); console.log(params.get('abc')); // 結果 // 123
引数のkeyのvalueを全て返すgetAll
getと何が違うのかという話なので、サンプルを2つ作ってみました。
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); console.log(params.getAll('abc')) // 結果 // [ '123' ]
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789&abc=777'; const params = new URLSearchParams(qs); console.log(params2.getAll('abc')) // 結果 // [ '123', '777' ]
URLSearchParamsオブジェクトは、キーの重複を許容するので、getAllメソッドが用意されています。
ちなみに、getメソッドの場合、重複したキーのうち、最初に登録されたものを返す仕様なので、getAllやループ内でしか値にアクセスできない事がありえます。
存在確認するhas
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); console.log(params.has('abc')); // 結果 // true
キーのイテレータを返すkeys
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); for (let k of params.keys()) console.log(k); // 結果 /* abc xyz aaa */
上書きするset
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); params.set('vvv', 247); console.log(params.toString()); // 結果 // abc=123&xyz=456&aaa=789&vvv=247 params.set('vvv', 247); console.log(params.toString()); // 結果 // abc=123&xyz=456&aaa=789&vvv=247
対応するキーが存在しない場合は、append相当の動きをし、キーが存在する場合は、キーの内容を上書きします。
複数キーが存在している場合は、全て削除した後、appendするようです。
const qs = 'a=1&a=2&a=3'; const params = new URLSearchParams(qs); console.log(params.toString()); // この時点では // a=1&a=2&a=3 params.set('a', 4); console.log(params.toString()); // 結果 // a=4
破壊的ソートのsort
オブジェクトの中身を名前順に正順ソートします。
逆順ソートはできないようです。
オブジェクトの順番を入れ替えたURLSearchParamsオブジェクトを返してくれるのではなく、実行したオブジェクトの順序を入れ替える点にご注意(順序が気になる事はあまりないですが)。
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); console.log(params.toString()); params.sort(); console.log(params.toString()); // 結果 // 実行前 // abc=123&xyz=456&aaa=789&vvv=247 // 実行後 // aaa=789&abc=123&vvv=247&xyz=456
値のイテレータを返すvalues
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); for (let v of params.values()) console.log(v); // 結果 /* 789 123 247 456 */
URLSearchParamsオブジェクト自体がイテラブル
const {URLSearchParams} = require('url'); const qs = 'abc=123&xyz=456&aaa=789'; const params = new URLSearchParams(qs); for (const [k, v] of params) console.log(k, v); // 結果 /* aaa 789 abc 123 vvv 247 xyz 456 */
まとめ
これからのURLパースで重要な位置を占めるであろう、WHATWG URL APIとURLSearchParamsについて、まとめてみましたが、おわかりいただけたでしょうか。
意外に面倒だったクエリストリング周りの実装が捗りそうですね。
以上です。