[Node.js] 新旧APIでのURLパースの違い

[千 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/sen)の19日目の記事です。
Node.jsでurlのパースをするには2通りの方法があると知り、それぞれ何が違うのか調べてみた内容を書きたいと思います。

# 背景

Electronアプリでaws-sdkをプロキシに対応させるために、以下のようなことをしていました。
そこでurlのパースを行なったときに直面したお話です。

```js
const AWS = require('aws-sdk')
const url = require('url')
const HttpsProxyAgent = require('https-proxy-agent')

const proxyUrl = url.parse('http://proxyurl:8080')
proxyUrl.rejectUnauthorized = false // 証明書によるエラーを無視

AWS.config.update({
  httpOptions: {
    agent: HttpsProxyAgent(proxyUrl) 
  }
})
```

プロキシ対応の内容はこちらとだいたい同じです。
→ [Using the AWS SDK for JavaScript from Behind a Proxy](https://aws.amazon.com/jp/blogs/developer/using-the-aws-sdk-for-javascript-from-behind-a-proxy/)

# `url.parse()` と `new URL()`

URLのパースのために、

```js
url.parse('http://proxyurl:8080')
```
を使っていた所、これはLegacy APIなので↓が良いのでは？とご指摘を受けました。

```js
new URL('http://proxyurl:8080')
```

やってることは同じパースなら書き換えても変わらないだろうと思ったのですが、やってみると何やら挙動が変わりました。

## ２つのパースはどう違うのか

まず、`url.parse()`の結果はこうなります。

```js
> url.parse('http://proxyurl:8080')
Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'proxyurl:8080',
  port: '8080',
  hostname: 'proxyurl',
  hash: null,
  search: null,
  query: null,
  pathname: '/',
  path: '/',
  href: 'http://proxyurl:8080/' }
```

次に、`new URL()`はこうです。

```js
> new URL('http://proxyurl:8080')
URL {
  href: 'http://proxyurl:8080/',
  origin: 'http://proxyurl:8080',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'proxyurl:8080',
  hostname: 'proxyurl',
  port: '8080',
  pathname: '/',
  search: '',
  searchParams: URLSearchParams {},
  hash: '' }
```

大体は同じですが、Legacy APIの方では`auth`だったのが、WHATWG APIでは`username`と`password`に別れたりと若干違います。
どちらも`hoge`という変数に代入すると`hoge.protocol`で`'http:'`の結果が得られます。

### プロパティの変更

それぞれでポート番号を変更してみます。

```js
> hoge = url.parse('http://proxyurl:8080')
> hoge.port = '9090'
> hoge
Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'proxyurl:8080',
  port: '9090',
  hostname: 'proxyurl',
  hash: null,
  search: null,
  query: null,
  pathname: '/',
  path: '/',
  href: 'http://proxyurl:8080/' }
```

`url.parse()`は変更すると、portが指定通り変わりました。
`new URL()`の方は、変更すると他のプロパティに表示されるポート番号も合わせて変わりました。

```js
> hoge = new URL('http://proxyurl:8080')
> hoge.port = '9090'
> hoge
URL {
  href: 'http://proxyurl:9090/',
  origin: 'http://proxyurl:9090',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'proxyurl:9090',
  hostname: 'proxyurl',
  port: '9090',
  pathname: '/',
  search: '',
  searchParams: URLSearchParams {},
  hash: '' }
```

### プロパティの削除

`url.parse()`の場合

```js
> hoge = url.parse('http://proxyurl:8080')
> delete hoge.port // true
> hoge // portは削除されている
Url {
  protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'proxyurl:8080',
  hostname: 'proxyurl',
  hash: null,
  search: null,
  query: null,
  pathname: '/',
  path: '/',
  href: 'http://proxyurl:8080/' }
```

`new URL()`の場合

```js
> hoge = new URL('http://proxyurl:8080')
> delete hoge.port // true
> hoge // portは残ったまま
URL {
  href: 'http://proxyurl:8080/',
  origin: 'http://proxyurl:8080',
  protocol: 'http:',
  username: '',
  password: '',
  host: 'proxyurl:8080',
  hostname: 'proxyurl',
  port: '8080',
  pathname: '/',
  search: '',
  searchParams: URLSearchParams {},
  hash: '' }
```

`delete　hoge.xxx`の結果はいずれもtrueが返ります。
ですが、`new URL()`の方は実際には削除されません。

### `Object.assign()`の結果

まずは`url.parse()`

```js
> huga = Object.assign({}, url.parse('http://proxyurl:8080'))
{ protocol: 'http:',
  slashes: true,
  auth: null,
  host: 'proxyurl:8080',
  port: '8080',
  hostname: 'proxyurl',
  hash: null,
  search: null,
  query: null,
  pathname: '/',
  path: '/',
  href: 'http://proxyurl:8080/' }
```

次に`new URL()`

```js
> huga = Object.assign({}, new URL('http://proxyurl:8080'))
{ [Symbol(context)]: 
   URLContext {
     flags: 400,
     scheme: 'http:',
     username: '',
     password: '',
     host: 'proxyurl',
     port: 8080,
     path: [ '' ],
     query: null,
     fragment: null },
  [Symbol(query)]: URLSearchParams {} }
```

結構違いがあります。

`Symbol()`ってなに…？と思い調べて、Symbol型というものの存在を知りました。
Symbolについてはこちらの記事を読みました。→ [ECMAScript6にシンボルができた理由](https://qiita.com/naruto/items/312adeb6145eb6221be7)

上の記事で
>シンボルをキーとしてオブジェクトに保存された値は、一般的な for ... in ループで列挙されない。作成したシンボル（つまり上記のfoo）をスコープの外に出るなどして忘れてしまったら、既存の方法で値を参照することは二度と不可能になる。

と書かれている通り、キーとなるシンボル`Symbol(context)`を参照できないと、このオブジェクトの値を参照できなくなってしまうようです。


# なぜ挙動が変わったのか

https-proxy-agentというモジュールを使っていたのですが、このモジュールでは引数で渡したurlオブジェクトに対して`Object.assign()`していました。

https://github.com/TooTallNate/node-https-proxy-agent/blob/11bc3478fdff1eec108cde4607bf10298be65ca3/index.js#L35

`url.parse()`であれば問題は無いのですが、`new URL()`だと`Object.assign()`後に値を参照できなくなり、得られる結果が変わってしまったのが原因でした。

# Legacy API と WHATWG API

Node.jsのドキュメントを読んで、urlモジュールにはLegacy API と Node.js 8.0で正式採用されたWHATWG API　の2つのAPIがあるとわかりました。
`url.parse()`はLegacy API、`new URL()`はWHATWG APIの書き方だそうです。

>The url module provides two APIs for working with URLs: a legacy API that is Node.js specific, and a newer API that implements the same WHATWG URL Standard used by web browsers.

Legacy APIの方は、廃止されてはいませんが既存アプリケーションの下位互換性のために残っているようです。
新規のアプリケーションはWHATWG APIの使用を推奨していました。

>While the Legacy API has not been deprecated, it is maintained solely for backwards compatibility with existing applications. New application code should use the WHATWG API.

なので今回は、https-proxy-agentとの互換性のために`url.parse()`を使うことにしました。

# おわり

初のQiita投稿でしどろもどろな感じですが、おかしなところは随時修正していきます…(；´Д｀)