最新版のnode.jsでは、async/awaitがサポートされてるらしいですね。 素晴らしい!!
でも最新版が常に使えるわけではないというのが世の中です。 例えば現時点では Azure Functions だと node.js のバージョンは 6.5.0 固定みたいです。(そのうちバージョンも上がるだろうし、将来的には任意のバージョンを選択することが出来るようになるでしょうけど)
そんな環境では、非同期処理のコールバック地獄を避けるために小細工が必要になります。(地獄にダイブしてもいいけど)
ということで非同期処理を扱うライブラリの async を軽く触ってみました。
async
以下のコマンドで導入可能です。
npm i async
割とドキュメントもしっかり書かれてる印象です。
基本コンセプトとしては関数の配列を渡して、それを順次実行したり並列実行したりといったものになりそうです。 一番基本的なのは series メソッドになると思います。見たほうが早いと思うので書いてみます。
var async = require('async'); function main() { async.series([function (callback) { console.log('process 1 at ', newDate().toISOString()); setTimeout(function() { callback(null, 1); }, 1000); }, function (callback) { console.log('process 2 at ', newDate().toISOString()); setTimeout(function() { callback(null, 2); }, 1000); }, function (callback) { console.log('process 3 at ', newDate().toISOString()); setTimeout(function() { callback(null, 3); }, 1000); }, ], function(error, results) { console.log('completed at ', newDate().toISOString()); console.log('results is ', JSON.stringify(results)); }); } main();
series には順次実行していきたい関数を配列で渡して、最後に完了処理の関数を渡します。 配列で渡した関数は callback を引数で受け取り、これを呼び出すことで処理の完了を通知するという感じです。
callback には第一引数にエラー、第二引数に結果を渡します。
series 関数の第二引数は必須ではないのですが全ての処理が終わった後に呼ばれる関数を指定できます。エラーと結果を受け取ります。
実行すると1秒ごとにメッセージが出てきます。
process 1 at 2017-08-23T05:30:23.618Z process 2 at 2017-08-23T05:30:24.620Z process 3 at 2017-08-23T05:30:25.620Z completed at 2017-08-23T05:30:26.621Z results is [1,2,3]
series がシーケンシャルに処理を実行したのに対して parallel では並列実行するというイメージです。 先ほどの series の呼び出しを parallel に変更してみましょう。
var async = require('async'); function main() { async.parallel([function (callback) { console.log('process 1 at ', newDate().toISOString()); setTimeout(function() { callback(null, 1); }, 1000); }, function (callback) { console.log('process 2 at ', newDate().toISOString()); setTimeout(function() { callback(null, 2); }, 1000); }, function (callback) { console.log('process 3 at ', newDate().toISOString()); setTimeout(function() { callback(null, 3); }, 1000); }, ], function(error, results) { console.log('completed at ', newDate().toISOString()); console.log('results is ', JSON.stringify(results)); }); } main();
結果はこうなります。
process 1 at 2017-08-23T05:33:29.889Z process 2 at 2017-08-23T05:33:29.891Z process 3 at 2017-08-23T05:33:29.892Z completed at 2017-08-23T05:33:30.892Z results is [1,2,3]
process 1 - 3 が同時に開始されてることがわかります。
ということで何か処理をやって結果を集めて最終処理をしたいというときにとても便利な関数ですね。普通のコールバックで結果を通知する API を使ってこれをやるのはめんどくさそう。
因みに非同期処理の結果を受け取り続きの処理を行いたい!ということもよくあると思います。 それをやってくれるのが waterfall メソッドになります。
これは callback 引数に渡されたものを後続の関数に渡してくれます。やってみましょう。
var async = require('async'); function main() { async.waterfall([function (callback) { console.log('process 1 at ', newDate().toISOString()); setTimeout(function() { callback(null, 1); }, 1000); }, function (input, callback) { console.log('process 2 at ', newDate().toISOString()); console.log('input: ', input); setTimeout(function() { callback(null, 2); }, 1000); }, function (input, callback) { console.log('process 3 at ', newDate().toISOString()); console.log('input: ', input); setTimeout(function() { callback(null, 3); }, 1000); }, ], function(error, result) { console.log('completed at ', newDate().toISOString()); console.log('results is ', JSON.stringify(result)); }); } main();
実行すると以下のようになります。
process 1 at 2017-08-23T05:38:06.428Z process 2 at 2017-08-23T05:38:07.429Z input: 1 process 3 at 2017-08-23T05:38:08.430Z input: 2 completed at 2017-08-23T05:38:09.430Z results is 3
ちゃんと後続の処理に値が渡ってることが確認できます。
因みに正常系を見てきましたが、エラーが発生したら callback の第一引数に何か渡してやると最後の後始末用の関数の第一引数にその値が渡ってきます。
var async = require('async'); function main() { async.waterfall([function (callback) { console.log('process 1 at ', newDate().toISOString()); setTimeout(function() { callback('error!!', 1); }, 1000); // callback の第一引数にはエラーがあったときに何か渡す}, function (input, callback) { console.log('process 2 at ', newDate().toISOString()); console.log('input: ', input); setTimeout(function() { callback(null, 2); }, 1000); }, function (input, callback) { console.log('process 3 at ', newDate().toISOString()); console.log('input: ', input); setTimeout(function() { callback(null, 3); }, 1000); }, ], function(error, result) { console.log('completed at ', newDate().toISOString()); console.log('results is ', JSON.stringify(result)); console.log('error is ', JSON.stringify(error)); }); } main();
実行するとこうなります。
process 1 at 2017-08-23T05:41:22.452Z completed at 2017-08-23T05:41:23.453Z results is 1 error is "error!!"
ここまで紹介したのが Control Flow に属する関数の一部で、この他に Collections というカテゴリの関数もあります。これは、名前の通りコレクションをインプットにしていい感じに関数を呼んでくれます。
一番単純だと思う map 関数を試してみましょう。
var async = require('async'); function main() { async.map([1, 2, 3], function(input, callback) { console.log(newDate().toISOString(), ' process started: ', input); callback(null, input * input); }, function(error, results) { console.log('error is ', JSON.stringify(error)); console.log('results is ', JSON.stringify(results)); }); } main();
実行すると配列の中身に対してどばっと処理をして、全て終わったら最終処理に結果を渡してくれるという動きをしてることが確認できます。
2017-08-23T05:46:03.558Z process started: 1 2017-08-23T05:46:03.560Z process started: 2 2017-08-23T05:46:03.560Z process started: 3 error is null results is [1,4,9]
実際に使ってみよう
ということで試しに Azure のテーブルストレージにデータを突っ込むというのをやってみたいと思います。 azure-storage というモジュールとUUIDを生成するための uuid というモジュールをインストールします。
npm i azure-storage npm i uuid
そして、azure-sorage のドキュメントを見ながら適当にデータを突っ込む処理を書いてみましょう。
var async = require('async'); var uuid = require('uuid/v4'); var azure = require('azure-storage'); function main() {var tableService = azure.createTableService('<storage account name>', '<storage account key>'); var entGen = azure.TableUtilities.entityGenerator; async.waterfall([function(callback) { tableService.createTableIfNotExists('sample', callback); }, function(result, response, callback) { console.log('table created'); var entity = { PartitionKey: entGen.String('sample'), RowKey: entGen.String(uuid()), Message: entGen.String('Hello world at ' + newDate().toISOString()) }; tableService.insertEntity('sample', entity, callback); }, ], function(error, result, response) { console.log('error is ', JSON.stringify(error)); console.log('result is ', JSON.stringify(result)); console.log('response is ', JSON.stringify(response)); }); } main();
azure-storage のコールバックが第一引数にエラーを渡してきて、そのあとに色んな結果を渡すというインターフェースだったので async と相性いいですね。node.js というか JavaScript がそういう文化なのかな?
実行すると以下のような結果になります。
table created error is null result is {".metadata":{"etag":"W/\"datetime'2017-08-23T06%3A09%3A50.5318067Z'\""}} response is {"isSuccessful":true,"statusCode":204,"body":"","headers":{"cache-control":"no-cache","content-length":"0","etag":"W/\"datetime'2017-08-23T06%3A09%3A50.5318067Z'\"","location":"https://funcrelationacf6.table.core.windows.net/sample(PartitionKey='sample',RowKey='7b0ed570-f5a7-46ec-8ee1-62efefa02a66')","server":"Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0","x-ms-request-id":"b0720413-0002-0029-40d6-1bedaf000000","x-ms-version":"2017-04-17","x-content-type-options":"nosniff","preference-applied":"return-no-content","dataserviceid":"https://funcrelationacf6.table.core.windows.net/sample(PartitionKey='sample',RowKey='7b0ed570-f5a7-46ec-8ee1-62efefa02a66')","date":"Wed, 23 Aug 2017 06:09:50 GMT","connection":"close"}}
色々データの詰まったオブジェクトが返ってきてるのが感じ取れますね。
Storage Explorer でテーブルを覗いてみるとちゃんと入ってました!
いい感じですね。