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について、まとめてみましたが、おわかりいただけたでしょうか。
意外に面倒だったクエリストリング周りの実装が捗りそうですね。
以上です。
0