【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【導入実績 500社以上】AWS 構築・運用保守・監視サービス

【導入実績 500社以上】AWS 構築・運用保守・監視サービス

【圧倒的 低コスト】Oracle Cloud 構築・運用保守・監視サービス

【圧倒的 低コスト】Oracle Cloud 構築・運用保守・監視サービス

【WordPress専用】高速 クラウド / サーバー『WebSpeed』

【WordPress専用】高速 クラウド / サーバー『WebSpeed』

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【次世代】ゲーム専用データ分析エンジン『ThinkingEngine』

【次世代】ゲーム専用データ分析エンジン『ThinkingEngine』

【100URLの登録が0円】Webサイト監視サービス『Appmill』

【100URLの登録が0円】Webサイト監視サービス『Appmill』

【SNSアプリ開発】LINE カスタムアプリ開発サービス

【SNSアプリ開発】LINE カスタムアプリ開発サービス

【ECアプリ開発】Shopify カスタムアプリ開発サービス

【ECアプリ開発】Shopify カスタムアプリ開発サービス

【音声アプリ開発】Twilio カスタムアプリ開発サービス

【音声アプリ開発】Twilio カスタムアプリ開発サービス

【グローバル対応】北米リージョン・クラウド / サーバー サポート

【グローバル対応】北米リージョン・クラウド / サーバー サポート

【取材記事】サーバーサイド・バックエンドエンジニアを募集中

【取材記事】サーバーサイド・バックエンドエンジニアを募集中

【YouTube】ビヨンド公式チャンネル「びよまるチャンネル」

【YouTube】ビヨンド公式チャンネル「びよまるチャンネル」

Node.js 8.0で正式実装されたWHATWG URL APIを紹介!

こんにちは。
開発チームのワイルド担当、まんだいです。

少し前の話になりますが、2017年5月30日にNode.jsの8.0.0が公開されました。
このバージョンから、npmのバージョン5.0.0がバンドルされるようになり、キャッシュ周りのコードが書き直され、高速化されているようです。

過去のバージョンとの速度比較を行ったツイートも公開され、この例では、従来の1/5の速度でインストールが完了しているようです。

現在のバージョンの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
読み込み中...
0 票, 平均: 0.00 / 10
3,686
facebook twitter はてなブックマーク
【大阪 / 横浜】インフラエンジニア / サーバーサイドエンジニア 積極採用中!

【大阪 / 横浜】インフラエンジニア / サーバーサイドエンジニア 積極採用中!

この記事をかいた人

About the author

萬代陽一

ソーシャルゲームのウェブ API などの開発がメイン業務ですが、ありがたいことにマーケティングなどいろんな仕事をさせてもらえています。
なおビヨンド内での私の肖像権は CC0 扱いになっています。