9月 202014
色々なNodeのライブラリがそれぞれで好きなPromiseライブラリ使ってるけど、混ぜても大丈夫なのか?ちゃんと相互にthenでチェインしたりエラー捕まえたりできるのか?
という事と、async.eachSeriesのようにURLのリストを1つずつ順番に処理完了するのを待ちながら処理していくのはPromiseでどうやって書くのかな?
というのが気になっていたのでちょっと調べた。
https://github.com/shokai/promise-study
環境
node v0.10.29 + coffee-script v1.8.0なのでまだPromiseが標準ライブラリに入ってないNode環境
試したPromiseライブラリ
このへんが有名そうだったのでREADMEやサンプルなどを読んで、試した。でもdeferredはインタフェースがthennableじゃないっぽかったのですぐあきらめた。
結論
then/catchしたいだけならes6-promise使って、async.jsでやるような高機能な並列・並行処理の制御がしたければbluebird使えばいいと思う。
es6-promiseとbluebirdは混ぜこぜで使ってもちゃんと動いた。
Qはなぜかcatchでエラーが捕まえられなかった(俺が使い方間違ってるかもしれない)
Qが例外をキャッチできない
es6-promiseとbluebirdは混ぜてthen/catchでチェインさせても大丈夫
qはcatchでエラーと取れない
qの中でthrow Errorしてもcatchできなかった
試したコード
## いろいろなPromiseライブラリを使ってみる
## Qだけthrow new Errorが捕まえられなかった
{Promise} = require 'es6-promise'
# Promise = require 'q'
# Promise = require 'bluebird'
# 奇数かどうかをtrue/falseで返すPromise
# 数字じゃない物が渡されたらエラー返す
checkOdd = (num) ->
return new Promise (resolve) ->
if typeof num isnt 'number'
throw new Error "#{num} is not number"
resolve num % 2 is 1
for i in [0,1,2,3,null,5,"かずどん",7,8]
do (i) ->
checkOdd i
.then (res) ->
if res
console.log "#{i} is odd"
else
console.log "#{i} is not odd"
.catch (err) ->
console.error err
1つずつ処理する
HTTP Getして、HTMLからtitleを取り出して、それをmacのsayコマンドで読む。という3つのPromiseをつなげて1セットの処理として、URLリストを順番に処理していく。
RSSを順番に見ていくクローラー的な処理を想定していて、1つ終わったら3秒待ってから次のURLを見に行くようにした。
試したコード
ちなみにURLリスト全部一気に処理するversionもある
下の方でBluebird.eachを使って1つずつURLを処理していく。
thenのチェインが最後まで走ったら3秒待ってから次のURLを処理するし、どこか途中でエラーが起きたらcatchして5秒待って次のURLの処理に行く。とにかく同時に複数のHTTPリクエストは送らない。
こういうのをasync.eachSeriesで書くとけっこう頭が疲れるコードになると思うけどPromise使ったらすんなり書けた。
es6-promiseとbluebirdを混ぜて使っているけどちゃんと動いた。
## HTMLを(1つずつ)取得してtitleを取り出してsayで読み上げる
## いろいろなPromiseライブラリを混ぜて使ってみる
## HTTPリクエストするのは3000 msecごと
## 途中でエラーがあったら5000 msec待ってから、次のHTTPリクエストする
request = require 'request'
cheerio = require 'cheerio'
{exec} = require 'child_process'
process.env.DEBUG ||= '*'
debug = require('debug')('promise-study')
{Promise} = require 'es6-promise'
Q = require 'q'
Bluebird = require 'bluebird'
urls = [
'http://shokai.org'
'そんなURLはない' # URLじゃない文字列
'https://github.com'
'https://github.com/robots.txt' # HTMLが返ってこない文字列
'https://google.co.jp'
]
# HTML本文を取得するPromise
# URLが間違っていたりすると失敗する
getHtml = (url) ->
debug "getHtml(#{url})"
return new Promise (resolve, reject) ->
request url, (err, res, body) ->
if err or res.statusCode isnt 200
reject(err or "statusCode: #{res.statusCode}")
resolve body
# HTMLからタイトルを取得するPromise
# HTMLじゃなければ失敗する
getTitle = (html) ->
debug "getTitle(html)"
return new Bluebird (resolve, reject) ->
$ = cheerio.load html
if title = $('title').text()
resolve title
return
reject 'title not found'
# 音声読み上げするPromise
speech = (txt) ->
debug "speech(#{txt})"
return new Promise (resolve, reject) ->
exec "say #{txt}", (err, stdout, stderr) ->
if err
reject(txt)
return
resolve(txt)
# URLリストをeachで1つずつ処理する
Bluebird.each urls, (url) ->
getHtml url
.then getTitle
.then speech
.then (title) ->
return new Promise (resolve) ->
debug "wait 3000 msec"
# 3秒待ってから次のURLの処理へ
setTimeout ->
debug "wait done"
debug "!!OK #{url} - #{title}"
resolve()
, 3000
.catch (err) ->
return new Promise (resolve, reject) ->
debug "!!ERROR #{url} - #{err}"
debug "wait 5000 msec for Error"
# どこかでエラーあったら5秒待つ
setTimeout ->
debug "wait done"
resolve()
, 5000
実行結果
ちゃんと3秒/5秒待って次、と順番に処理できている。requestにURLじゃないのを渡した時の例外もtry catch書かずにpromiseのcatchで捕捉できている。