Webサイトからテキストデータを取得するコードをプロミスを使って書き直してみた

以前ブログに投稿した[Webサイトからテキストデータを取得するコード](http://info-i.net/http-get-text)をプロミスを使って書き直してみました。

プロミスに関する説明はブログに書いています。

* [ECMAScript 6の新機能（２６）プロミス](http://info-i.net/ecmascript6-promise)

下が書き直してみたコードです。説明はもっと下にあります。

```js
var co = require('co');
var bluebird = require('bluebird');
var url = require('url');
var http = require('http');
var https = require('https');
var concatStream = require('concat-stream');
var pump = require('pump');
var mimelib = require('mimelib');
var jschardet = require('jschardet');
var iconvLite = require('iconv-lite');

function getUrl (option) {
	return co(function* () {
		option = combine(option, {});

		var res = (yield getRes(option)).res;
		var data = yield rstream2promise(res);
		if (res.statusCode === 200) {
			var encoding = undefined;
			if (res.headers['content-type'] !== undefined) {
				encoding = mimelib.parseHeaderLine(res.headers['content-type']).charset;
			}
			if (encoding === undefined) {
				encoding = jschardet.detect(data).encoding;
			}
			if (encoding !== undefined && iconvLite.encodingExists(encoding)) {
				option.data = iconvLite.decode(data, encoding);
				return option;
			}
			else {
				throw new Error('can\'t detect encoding.');
			}
		}
		else {
			throw new Error(res.statusMessage);
		}
	});
}

function getRes (option) {
	return new bluebird(function (resolve, reject) {
		option = combine(option, {
			url: 'https://www.google.co.jp/', 
			timeout: 10000
		});
		if (typeof option.url === 'string') {
			option.url = url.parse(option.url, true);
		}
		else if (typeof option.url === 'object') {
		}
		else {
			reject(new TypeError('unknown type.'));
		}
		if (option.url.protocol === 'http:') {
			option.module = http;
		}
		else if (option.url.protocol === 'https:') {
			option.module = https;
		}
		else {
			reject(new TypeError('unknown protocol.'));
		}

		var client = option.module.get(option.url, function (res) {
			option.res = res;
			resolve(option);
		});
		client.setTimeout(option.timeout, function () {
			client.abort();
			reject(new Error('timeouted.'));
		});
		client.on('error', function (err) {
			reject(err);
		});
	});
}

function rstream2promise (stream) {
	return new bluebird(function (resolve, reject) {
		pump(stream, concatStream(function (data) {
			resolve(data);
		}), function (err) {
			reject(err);
		});
	});
};

function combine (option, def) {
	if (option === undefined) {
		option = {};
	}
	for (key of Object.keys(def)) {
		if (option[key] === undefined) {
			option[key] = def[key];
		}
	}
	return option;
};
```

### 説明

```js
function getUrl (option) {
	return co(function* () {
・・・
	});
}
```

`getUrl`関数が本体です。この関数は第1引数としてオプションを受け取り、**Webサイトからテキストデータを取得するプロミス**を返します。

このプロミスは`co`モジュールの`co`関数を使用して作成しています。`co`関数に関しては下の記事に書きました。

* [Node.jsで複数のプロミスを同期的に実行する](http://info-i.net/co)

`getUrl`関数の中身を見ていきます。

```js
function getUrl (option) {
	return co(function* () {
		option = combine(option, {});
・・・
	});
}

function combine (option, def) {
	if (option === undefined) {
		option = {};
	}
	for (key of Object.keys(def)) {
		if (option[key] === undefined) {
			option[key] = def[key];
		}
	}
	return option;
};
```

`combine`関数は2つのオブジェクトを1つに纏めるだけの関数です。この関数で引数のオプションとデフォルトのオプションを結合しています。

```js
function getUrl (option) {
	return co(function* () {
・・・
		var res = (yield getRes(option)).res;
・・・
}
```

次に、`getRes`関数を使用しています。この関数の第1引数にオプションを渡し、返り値の`res`プロパティを取得しています。

`getRes`関数を見ましょう。

```js
function getRes (option) {
	return new bluebird(function (resolve, reject) {
・・・
	});
}
```

`getRes`関数はWebサイトにHTTPリクエストを送信し、HTTPレスポンスを取得する関数です。この関数は第1引数としてオプションを受け取り、**WebサイトにHTTPリクエストを送信するプロミス**を返します。

このプロミスは`bluebird`モジュールの`bluebird`関数を使用して作成しています。`bluebird`関数はプロミスを作成するための関数です。詳細は上のプロミスに関する記事を参照してください。

```js
function getRes (option) {
	return new bluebird(function (resolve, reject) {
		option = combine(option, {
			url: 'https://www.google.co.jp/', 
			timeout: 10000
		});
		if (typeof option.url === 'string') {
			option.url = url.parse(option.url, true);
		}
		else if (typeof option.url === 'object') {
		}
		else {
			reject(new TypeError('unknown type.'));
		}
		if (option.url.protocol === 'http:') {
			option.module = http;
		}
		else if (option.url.protocol === 'https:') {
			option.module = https;
		}
		else {
			reject(new TypeError('unknown protocol.'));
		}
・・・
	});
}
```

この関数では最初にオプションに対する処理を行っています。

`combine`関数を使用して引数のオプションとデフォルトのオプションを結合しています。

このオプションには`url`オプションと`timeout`オプションを指定することができます。

* `url`・・・HTTPリクエストのURLです。デフォルトは`https://www.google.co.jp/`です。
* `timeout`・・・HTTPリクエストの送信がタイムアウトする時間（ミリ秒単位）です。デフォルトは`10000`です。

`url`オプションの値が文字列である場合には`url.parse`関数を使用してパースします。

パースした結果の`protocol`プロパティが`http:`である場合にはHTTPリクエストを送信するのに`http`モジュールを使用することにします。`https:`である場合には`https`モジュールを使用することにします。

```js
function getRes (option) {
	return new bluebird(function (resolve, reject) {
・・・
		var client = option.module.get(option.url, function (res) {
			option.res = res;
			resolve(option);
		});
		client.setTimeout(option.timeout, function () {
			client.abort();
			reject(new Error('timeouted.'));
		});
		client.on('error', function (err) {
			reject(err);
		});
	});
}
```

`http.get`関数か`https.get`関数を使用してURLに対するHTTPリクエストを送信します。

HTTPリクエストに対するHTTPレスポンスが返ってきた場合には`option.res`にHTTPレスポンスを表すオブジェクトを格納し、`option`をプロミスの結果として返します。

また、`http.get`関数や`https.get`関数の返り値はHTTPクライアントを表すオブジェクトとなりますので、このオブジェクトの`setTimeout`関数を使用してタイムアウト時間を設定します。

タイムアウトした場合にはエラーを発生させます。

`getRes`の中身が分かったので`getUrl`関数に戻りましょう。

```js
function getUrl (option) {
	return co(function* () {
・・・
		var res = (yield getRes(option)).res;
		var data = yield rstream2promise(res);
・・・
	});
}

function rstream2promise (stream) {
	return new bluebird(function (resolve, reject) {
		pump(stream, concatStream(function (data) {
			resolve(data);
		}), function (err) {
			reject(err);
		});
	});
};
```

`getRes`関数の返り値の`res`プロパティはHTTPレスポンスを表すオブジェクトでしたが、これはHTTPレスポンスボディを読み込む読み込みストリームでもあります。

ストリームに関する説明はブログに書いています。

* [Node.jsのストリームの使い方](http://info-i.net/node-js-stream)

このストリームを`rstream2promise`関数の第1引数に渡しています。この関数は読み込みストリームをプロミスに変換する関数です。この関数に関しては下の記事を参照してください。

* [Node.jsで読み込みストリームをプロミスに変換する](http://info-i.net/stream-promise)

この関数の返り値のプロミスの結果としてHTTPレスポンスボディの内容が得られます。

```js
function getUrl (option) {
	return co(function* () {
・・・
		if (res.statusCode === 200) {
			var encoding = undefined;
			if (res.headers['content-type'] !== undefined) {
				encoding = mimelib.parseHeaderLine(res.headers['content-type']).charset;
			}
			if (encoding === undefined) {
				encoding = jschardet.detect(data).encoding;
			}
			if (encoding !== undefined && iconvLite.encodingExists(encoding)) {
				option.data = iconvLite.decode(data, encoding);
				return option;
			}
			else {
				throw new Error('can\'t detect encoding.');
			}
		}
		else {
			throw new Error(res.statusMessage);
		}
	});
}
```

後はHTTPレスポンスのステータスコードが`200`である場合にのみデータを適切な文字コードから`utf-8`に変換するだけです。変換した文字列を`option.data`に格納し、`option`をプロミスの結果として返します。