かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

JavaScript(node.js)で非同期処理を扱いたい(asyncモジュール使ってみたよ)

最新版のnode.jsでは、async/awaitがサポートされてるらしいですね。 素晴らしい!!

でも最新版が常に使えるわけではないというのが世の中です。 例えば現時点では Azure Functions だと node.js のバージョンは 6.5.0 固定みたいです。(そのうちバージョンも上がるだろうし、将来的には任意のバージョンを選択することが出来るようになるでしょうけど)

そんな環境では、非同期処理のコールバック地獄を避けるために小細工が必要になります。(地獄にダイブしてもいいけど)

ということで非同期処理を扱うライブラリの async を軽く触ってみました。

async

以下のコマンドで導入可能です。

npm i async

割とドキュメントもしっかり書かれてる印象です。

Home - Documentation

基本コンセプトとしては関数の配列を渡して、それを順次実行したり並列実行したりといったものになりそうです。 一番基本的なのは series メソッドになると思います。見たほうが早いと思うので書いてみます。

var async = require('async');

function main() {
    async.series([
        function (callback) {
            console.log('process 1 at ', new Date().toISOString());
            setTimeout(function() { callback(null, 1); }, 1000);
        },
        function (callback) {
            console.log('process 2 at ', new Date().toISOString());
            setTimeout(function() { callback(null, 2); }, 1000);
        },
        function (callback) {
            console.log('process 3 at ', new Date().toISOString());
            setTimeout(function() { callback(null, 3); }, 1000);
        },
    ],
    function(error, results) {
        console.log('completed at ', new Date().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 ', new Date().toISOString());
            setTimeout(function() { callback(null, 1); }, 1000);
        },
        function (callback) {
            console.log('process 2 at ', new Date().toISOString());
            setTimeout(function() { callback(null, 2); }, 1000);
        },
        function (callback) {
            console.log('process 3 at ', new Date().toISOString());
            setTimeout(function() { callback(null, 3); }, 1000);
        },
    ],
    function(error, results) {
        console.log('completed at ', new Date().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 ', new Date().toISOString());
            setTimeout(function() { callback(null, 1); }, 1000);
        },
        function (input, callback) {
            console.log('process 2 at ', new Date().toISOString());
            console.log('input: ', input);
            setTimeout(function() { callback(null, 2); }, 1000);
        },
        function (input, callback) {
            console.log('process 3 at ', new Date().toISOString());
            console.log('input: ', input);
            setTimeout(function() { callback(null, 3); }, 1000);
        },
    ],
    function(error, result) {
        console.log('completed at ', new Date().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 ', new Date().toISOString());
            setTimeout(function() { callback('error!!', 1); }, 1000); // callback の第一引数にはエラーがあったときに何か渡す
        },
        function (input, callback) {
            console.log('process 2 at ', new Date().toISOString());
            console.log('input: ', input);
            setTimeout(function() { callback(null, 2); }, 1000);
        },
        function (input, callback) {
            console.log('process 3 at ', new Date().toISOString());
            console.log('input: ', input);
            setTimeout(function() { callback(null, 3); }, 1000);
        },
    ],
    function(error, result) {
        console.log('completed at ', new Date().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(new Date().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 ' + new Date().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 でテーブルを覗いてみるとちゃんと入ってました!

f:id:okazuki:20170823151352p:plain

いい感じですね。