以前ブログに投稿したWebサイトからテキストデータを取得するコードをプロミスを使って書き直してみました。
プロミスに関する説明はブログに書いています。
下が書き直してみたコードです。説明はもっと下にあります。
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;
};
説明
function getUrl (option) {
return co(function* () {
・・・
});
}
getUrl
関数が本体です。この関数は第1引数としてオプションを受け取り、Webサイトからテキストデータを取得するプロミスを返します。
このプロミスはco
モジュールのco
関数を使用して作成しています。co
関数に関しては下の記事に書きました。
getUrl
関数の中身を見ていきます。
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つに纏めるだけの関数です。この関数で引数のオプションとデフォルトのオプションを結合しています。
function getUrl (option) {
return co(function* () {
・・・
var res = (yield getRes(option)).res;
・・・
}
次に、getRes
関数を使用しています。この関数の第1引数にオプションを渡し、返り値のres
プロパティを取得しています。
getRes
関数を見ましょう。
function getRes (option) {
return new bluebird(function (resolve, reject) {
・・・
});
}
getRes
関数はWebサイトにHTTPリクエストを送信し、HTTPレスポンスを取得する関数です。この関数は第1引数としてオプションを受け取り、WebサイトにHTTPリクエストを送信するプロミスを返します。
このプロミスはbluebird
モジュールのbluebird
関数を使用して作成しています。bluebird
関数はプロミスを作成するための関数です。詳細は上のプロミスに関する記事を参照してください。
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
モジュールを使用することにします。
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
関数に戻りましょう。
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レスポンスボディを読み込む読み込みストリームでもあります。
ストリームに関する説明はブログに書いています。
このストリームをrstream2promise
関数の第1引数に渡しています。この関数は読み込みストリームをプロミスに変換する関数です。この関数に関しては下の記事を参照してください。
この関数の返り値のプロミスの結果としてHTTPレスポンスボディの内容が得られます。
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
をプロミスの結果として返します。