かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

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

1つ前のエントリで async モジュール使ってみました。

blog.okazuki.jp

関数の配列を渡してやれば、よしなに実行してくれるいいやつでした。次は co です!

www.npmjs.com

async が古き良き仕組みの上に非同期処理をいい感じに扱うという観点のライブラリという雰囲気を感じましたが co は async/await は無いけど Promise とジェネレーターを使って非同期処理をいい感じに書こうという雰囲気です。

ジェネレーターについては MDN がいい感じにまとまってます。

developer.mozilla.org

co

以下のコマンドで導入でいます。

npm i co

co は非常にシンプルで co の引数にジェネレーター関数を渡すだけです。そして戻り値に対してthenを呼び出して正常に完了したときの処理とエラーが起きたときの処理を渡します。 なので、基本的にこうなります。

co(function*() { ... })
.then(function(result) {}, function(error) {});

ということで試しにやってみましょう。

var co = require('co');

function main() {
    co(function*() {
        var process1 = yield Promise.resolve(1);
        var process2 = yield Promise.resolve(2);
        var process3 = yield Promise.resolve(3);
        return [process1, process2, process3];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行すると以下のように表示されます。

[1,2,3]

試しに例外を発生させてみます。

var co = require('co');

function main() {
    co(function*() {
        var process1 = yield Promise.resolve(1);
        var process2 = yield Promise.resolve(2);
        var process3 = yield Promise.resolve(3);
        throw new Error('oops!!');
        return [process1, process2, process3];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行するとエラーのほうのフローにわたってることが確認できます。

error:  oops!!

実際に非同期処理っぽいことをしてみましょう。setTimeoutをラップした関数を用意して重たい処理をエミュレートします。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

function main() {
    co(function*() {
        var process1 = yield asyncProcess('process1');
        var process2 = yield asyncProcess('process2');
        var process3 = yield asyncProcess('process3')
        return [process1, process2, process3];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行するとこうなります。

["process1 completed at 2017-08-23T06:36:50.560Z","process2 completed at 2017-08-23T06:36:51.560Z","process3 completed at 2017-08-23T06:36:52.561Z"]

Promise の配列に対しても co の中で yield 出来たりします。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

function main() {
    co(function*() {
        return yield [
            asyncProcess('process1'),
            asyncProcess('process2'),
            asyncProcess('process3'),
        ];
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

この場合は当然ですがprocess1 - 3 はパラレルに走りますね。

["process1 completed at 2017-08-23T06:39:23.346Z","process2 completed at 2017-08-23T06:39:23.347Z","process3 completed at 2017-08-23T06:39:23.347Z"]

さらにプロパティが Promise を含むオブジェクトに対しても yield 出来ます。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

function main() {
    co(function*() {
        return yield {
            process1: asyncProcess('process1'),
            process2: asyncProcess('process2'),
            process3: asyncProcess('process3'),
            other: 'other value',
        };
    }).then(function(result) {
        console.log(JSON.stringify(result));
    }, function(error){
        console.log('error: ', error.message);
    });
}

main();

実行するとこんな感じです。

{"process1":"process1 completed at 2017-08-23T06:41:23.638Z","process2":"process2 completed at 2017-08-23T06:41:23.638Z","process3":"process3 completed at 2017-08-23T06:41:23.638Z","other":"other value"}

この他にもジェネレータ関数を Promise を返す関数にしてくれる co.wrap(function*(arg) {}) というのがあります。

var co = require('co');

function asyncProcess(processName) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(`${processName} completed at ${new Date().toISOString()}`);
        }, 1000);
    });
}

// (number, number): Promise<number> みたいな
var asyncDiv = co.wrap(function* (x, y) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            if (y == 0) {
                return reject('divide by zero');
            }
            resolve(x / y);
        }, 1000);
    });
});

function main() {
    asyncDiv(10, 2).then(function(answer) { 
        console.log('answer: ', answer); 
    }, function(error) { 
        console.log('error: ', error); 
    });
    asyncDiv(10, 0).then(function(answer) { 
        console.log('answer: ', answer); 
    }, function(error) { 
        console.log('error: ', error); 
    });
}

main();

実行するとこうなります。

answer:  5
error:  divide by zero

実際に使ってみよう

ということで async のほうでもやった azure-storage を使った例を co でもやってみます。 こんな雰囲気ですかね?

var co = require('co');
var uuid = require('uuid/v4');
var azure = require('azure-storage');

function main() {
    co(function*() {
        var tableService = azure.createTableService('<accont name>', '<key>');
        var entGen = azure.TableUtilities.entityGenerator;
        yield new Promise(function(resolve, reject) {
            tableService.createTableIfNotExists('cosample', function(error, result) {
                if (error) {
                    return reject(error);
                }
                resolve(result);
            });
        });
        console.log('table created');
        var entity = {
            PartitionKey: entGen.String('sample'),
            RowKey: entGen.String(uuid()),
            Message: entGen.String('Hello world at ' + new Date().toISOString())
        };
        return yield new Promise(function(resolve, reject) {
            tableService.insertEntity('cosample', entity, function(error, result, response) {
                if (error) {
                    return reject(error);
                }
                resolve([result, response]);
            });
        });
    }).then(function(result) {
        console.log('result is ', JSON.stringify(result[0]));
        console.log('response is ', JSON.stringify(result[1]));
    }, function(error) {
        console.log('error is ', JSON.stringify(error));
    });
}

main();

実行すると、こんな感じになりました。

table created
result is  {".metadata":{"etag":"W/\"datetime'2017-08-23T07%3A03%3A01.8221368Z'\""}}
response is  {"isSuccessful":true,"statusCode":204,"body":"","headers":{"cache-control":"no-cache","content-length":"0","etag":"W/\"datetime'2017-08-23T07%3A03%3A01.8221368Z'\"","location":"https://funcrelationacf6.table.core.windows.net/cosample(PartitionKey='sample',RowKey='bd89259e-442c-4f20-b2cb-6180d15fcfbd')","server":"Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0","x-ms-request-id":"07a43392-0002-0002-70dd-1b9917000000","x-ms-version":"2017-04-17","x-content-type-options":"nosniff","preference-applied":"return-no-content","dataserviceid":"https://funcrelationacf6.table.core.windows.net/cosample(PartitionKey='sample',RowKey='bd89259e-442c-4f20-b2cb-6180d15fcfbd')","date":"Wed, 23 Aug 2017 07:03:01 GMT","connection":"close"}}

azure-storage モジュールの API が Promise ベースじゃないので幸福度が上がりませんね。 なので、自前で Promise が戻り値になるような API にラップしてみました。 ついでに、ここにきてアロー関数も使ってみました。

var co = require('co');
var uuid = require('uuid/v4');
var azure = require('azure-storage');

class MyTableService {
    get tableService() {
        return this._tableService ||
            (this._tableService = azure.createTableService('funcrelationacf6', 'Fg3DQ0pfmJGMKrpdd1IcIOQ8brUsz2/BBTHlctx/RClAlkwJYZkW+6c5m1GnC7+cKYIXhkbxQ1CMJSNwRE2fbg=='));
    }

    createTableIfNotExists() {
        return new Promise((resolve, reject) => {
            this.tableService.createTableIfNotExists('cosample', (error, result, response) => {
                if (error) {
                    return reject(error);
                }
                resolve({
                    result: result,
                    response: response
                });
            });
        });
    }

    insertEntity(entity) {
        var entGen = azure.TableUtilities.entityGenerator;
        return new Promise((resolve, reject) => {
            var azureStorageEntity = {
                PartitionKey: entGen.String(entity.PartitionKey),
                RowKey: entGen.String(entity.RowKey),
                Message: entGen.String(entity.Message)
            };
            this.tableService.insertEntity('cosample', azureStorageEntity, (error, result, response) => {
                if (error) {
                    return reject(error);
                }
                resolve({
                    result: result,
                    response: response
                });
            });
        });
    }
}

function main() {
    co(function* () {
        try {
            var tableService = new MyTableService();
            yield tableService.createTableIfNotExists();
            console.log('table created');
            var entity = {
                PartitionKey: 'sample',
                RowKey: uuid(),
                Message: `Hello world at ${new Date().toISOString()}`
            };
            var r = yield tableService.insertEntity(entity);
            console.log(`result is ${JSON.stringify(r.result)}`);
            console.log(`response is ${JSON.stringify(r.response)}`);
        } catch (error) {
            console.log(`error: ${error.message}`);
        }
    });
}

main();

ラップするのは苦行だけど悪くないね。

ラップの自動化

世の中には怠け者(誉め言葉)がいるみたいで、このラッピングを自動でしてくれるようなライブラリ作者がいるみたいです。 bluebird モジュールがそれになるみたいです。早速入れてみましょう。

npm i bluebird

これを使うと promisifyAll でさくっと Promise 化してくれます。

var co = require('co');
var uuid = require('uuid/v4');
var azure = require('azure-storage');
var Promise = require('bluebird');

function main() {
    co(function* () {
        try {
            var entGen = azure.TableUtilities.entityGenerator;
            var tableService = Promise.promisifyAll(azure.createTableService('<storage account>', '<key>'));
            yield tableService.createTableIfNotExistsAsync('bluebirdsample');
            console.log('table created');
            var entity = {
                PartitionKey: entGen.String('sample'),
                RowKey: entGen.String(uuid()),
                Message: entGen.String(`Hello world at ${new Date().toISOString()}`)
            };
            var r = yield tableService.insertEntityAsync('bluebirdsample', entity);
            console.log(`result is ${JSON.stringify(r)}`);
        } catch (error) {
            console.log(`error: ${error.message}`);
        }
    });
}

main();

ただ、azure-storage のコールバックは(error, result, response)という3つの引数を受け取るものが多い印象なのですが、こういうインターフェースだと response がロストしてるように感じます。う~む。

preview 版を使う??

まぁ、こういう不満は皆持ってるみたいで azure-storage 2.0.0-preview で Promise 対応が入ってます。

github.com

github.com

ただ、4月に preview が出てから4か月たってるけど正式リリースはまだみたいですね? そして、これは azure という超巨大モジュールの preview には適用されてるけど azure-storage モジュールには適用されてなさそう?

次は、これを調べてみたいと思います。今回は co がメインだしね!

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

いい感じですね。

Azure Functions でスロットを使ってみよう(プレビュー)

Azure Functions は、まだスロットに正式対応していません。現時点ではプレビュー扱いです。 ということで早速使ってみましょう。

スロットの有効化

Function App を作ってポータルで開きます。 そして Function settings を開いてスロットのプレビューをオンにします。

f:id:okazuki:20170821221618p:plain

割と色々と気をつけろよ!みたいなことを書いてる割には押してしまったらなんの確認メッセージも出ずにさくっと有効化されました。人によっては間違えて押しちゃいそうですね…気を付けないと。

スロットの作成

ということで staging スロットを作成してみましょう。 スロットの欄の横にあるプラス記号を押します。

f:id:okazuki:20170821221822p:plain

そうするとスロットの作成画面が開きます。 まぁ入力項目は名前だけなので迷うことはないですよね。

f:id:okazuki:20170821221931p:plain

デプロイするプロジェクトの作成

では staging にデプロイするための関数を作りましょう。 GetMessage という以下のような関数をローカルで定義してみました。

module.exports = function (context, req) {
    context.res = {
        status: 200,
        body: `${process.env.message} v1`,
    };
    context.done();
};

message の定義を返してるだけのシンプルな関数です。 この関数の定義の入ったリポジトリを Visual Studio Team Services あたりに作ります。

staging の展開オプションから作成したリポジトリを展開元として関連付けます。 そして、アプリケーション設定に message の値を追加します。本番に行ってほしくない構成はスロットの設定にしておきます。

f:id:okazuki:20170821223923p:plain

production にも追加しておきます。

f:id:okazuki:20170821224104p:plain

Postman でURLをたたきます。いい感じ。

f:id:okazuki:20170821223159p:plain

スワップ

staging と production を入れ替えてみましょう。staging を選択するとスワップというボタンがあります。

f:id:okazuki:20170821223404p:plain

次の画面でもう一度スワップボタンを押すと、どことどこを入れ替えるか選択する画面になります。

f:id:okazuki:20170821223514p:plain

因みにスワップには2種類あって普通に入れ替えるだけのスワップと、入れ替えは実施せずにアプリケーションの構成だけ入れ替え先のを持ってくるプレビューでのスワップがあります。

f:id:okazuki:20170821223615p:plain

せっかくなので、プレビューでのスワップを選びます。そして警告メッセージとか致命的なものが出てないのか確認します。

f:id:okazuki:20170821224347p:plain

今回は警告が出てますが、まぁいっかって感じなのでそのままOKを選択します。そうすると staging 環境のアプリケーション設定が production のものに上書きされます。

f:id:okazuki:20170821224538p:plain

Postman で URL をたたいてみます。このとき呼び出しに必要な code が変わってるので注意しましょう。

f:id:okazuki:20170821224715p:plain

URL は staging なので出力が production の設定を読み取って返していることが確認できます。

プレビュー品質っぽいところ?

動作確認できたのでスワップを完了させたいと思います。 一度関数の URL を確認するためにスワップの画面を離れてしまった場合関数のポータル画面から素直にスワップボタンを押せなくなっています。

f:id:okazuki:20170821224925p:plain

なのでプラットフォーム設定 → すべての設定 → 画面上部のスワップボタンからスワップの完了か取り消しが行える画面に行けます。

f:id:okazuki:20170821225132p:plain

スワップの取り消しを行うと、staging 環境のアプリケーション設定がもとに戻ります。スワップの完了を選択してOKを押すとスワップが完了して環境が入れ替わります。

f:id:okazuki:20170821225442p:plain

production の関数をたたくとばっちり動きますね。

f:id:okazuki:20170821225625p:plain

これで、また新しいバージョンを VSTS に push すると staging に新しいバージョンが配備されます。 そしてスワップすると、更新バージョンがプロダクションに行きます。

いい感じですね。早く正式リリースされないかな。

Azure Functions で Application Insights のアラートを使ってみよう

先日 Azure Functions で Application Insights の連携について書きました。 ということで今日はアラート使ってみようと思います。

Application Insights でアラート

アラートとかAlertとかドキュメントでは書かれていますがポータル上では警告です。はい。 Application Insights のブレードで警告を開いてプラスボタンでルールを追加します。ルールでは、関数の応答時間の平均などを閾値に指定できます。

例えばTimerFuncという関数を作ってみたのですが、こういう感じのルールで平均の応答時間が5000ms以上の時にメール通知とかっていう設定が出来ます。Webhookとかもキック出来るみたいですね。

f:id:okazuki:20170821215849p:plain

そして、TimerFuncのコードをこうしてみました。

module.exports = function (context, myTimer) {
    var timeStamp = new Date().toISOString();
    
    if(myTimer.isPastDue)
    {
        context.log('JavaScript is running late!');
    }
    context.log('JavaScript timer trigger function ran!', timeStamp);   
    setTimeout(function() {
        context.done();
    }, 6000);
};

これで確実にアラート出ますね!! 暫く待ってるとメールが来た!!

f:id:okazuki:20170821220436p:plain

ポータル上でも何かあったことが確認できます。

f:id:okazuki:20170821220543p:plain

えっ、ナニコレめっちゃいいんだけど。SIer時代に知っておくんだった…。

Azure Functions のログを Application Insights で見てみよう

Azure Functions のポータルのログを見るための仕組みっぽいモニターを開いたら Application Insights の使用をお勧めされたので使ってみます。

Azure Functions の作成時に Application Insights の ON/OFF が指定できるので、そこで指定するのが一番楽です。ですが、作ってしまったよ!というときでも後から追加できるのでやってみましょう。

Application Insights の作成

日本のリージョンは選択できないので適当なところに作ります。

作ったらブレードの概要のところにインストルメーテンション キーが表示されるので、それを控えておきます。

Functions へキーの設定

Function App を開いてアプリケーション設定を開きます。 アプリ設定にAPPINSIGHTS_INSTRUMENTATIONKEYを追加しましょう。値は先ほど取得したインストルメーテンション キーの値です。

これで Application Insights との連携は完了です。適当に関数をガンガン呼び出すスクリプトを走らせて Application Insights を覗くと動いてるのが確認できます。因みに、ちゃんと Function App が構成できてると、こんな風に構成済みの機能に Application Insights が追加されます。

f:id:okazuki:20170821001516p:plain

確認

開くと以下のような感じの画面になります。動いてる感が出てますね。

f:id:okazuki:20170821001726p:plain

メトリックス エクスプローラーを開いて適当にグラフを追加しましょう。HTTP要求実行時間とか。(最初から追加されてたっけ…?)

f:id:okazuki:20170821001907p:plain

グラフを選択するといい感じに結果が表示されます。

f:id:okazuki:20170821002039p:plain

下にスクロールすると、個別のログが出てきます。

f:id:okazuki:20170821003006p:plain

個別のログを選ぶと詳細が出ます。

f:id:okazuki:20170821003105p:plain

このトレース ステートメントがログに記録された要求をぽちっとすると要求の詳細が見えます。

f:id:okazuki:20170821003233p:plain

もう1段掘り下げると、この要求のすべてのトレースというのが出てきます。こいつをクリックすると同じ要求内のログが出てきます。

f:id:okazuki:20170821003430p:plain

いい感じ。

まとめ

ということで Application Insights を ON にしておくと色々はかどりそうです。アラート出すようにしたり、ログにクエリかけたりできるっぽい(勉強しなきゃ…)のでガンガン使いましょう。

Azure Functions のログを見てみる

Azure Functions で吐かれるログはポータルからリアルタイムに見ることが出来ますね。 これはこれで開発時に嬉しいのですが、じゃぁ運用時に障害発生したときに見るログとかはどうすればいいの?と思ってみてみました。

最初はApp Serviceのログの仕組みが使えるのかと思ってたのですが…

docs.microsoft.com

なんだか、これを有効化してもBlobにログが出てないように見えました。ということで、結局Functionsのログはどこに出てるの!?と探してみるとStorageアカウントのTableに吐き出されてました。

ドキュメントの何処に書いてあるんだろう… まだ、WebJobのドキュメントとかちゃんと見てないんだけど、そこらへんかしら?まぁ後で確認しないとだな…。

ということで、こんな感じのテーブルが出来ます。

f:id:okazuki:20170820164323p:plain

名前的に月単位で作られるのかな? 中身を見てみるとこんな感じになってます。

f:id:okazuki:20170820164516p:plain

ログレベル

error, warn, info, verboseがあります。 それぞれのレベルのログを出すには、以下のようにnode.jsだとcontext.logを呼び出します。

module.exports = function (context, req) {
    context.log.verbose(context.invocationId, ': verbose');
    context.log.info(context.invocationId, ': info');
    context.log.warn(context.invocationId, ': warn');
    context.log.error(context.invocationId, ': error');
    context.res = {
        status: 200,
        body: "invoked"
    };
    context.done();
};

ログ系メソッドは、引数を複数渡すと連結して出してくれるので便利ですね。あとinvocationIdは、関数の呼び出しごとに割り当てられる一位なIDがとれるっぽいので参考に出してます。

この関数をたたくとこんな感じにポータルのログ出力に出てきました。

2017-08-20T08:04:42.298 Function started (Id=c10fdebd-0c7f-4514-a39a-e7420edece7e)
2017-08-20T08:04:42.313 c10fdebd-0c7f-4514-a39a-e7420edece7e : verbose
2017-08-20T08:04:42.313 c10fdebd-0c7f-4514-a39a-e7420edece7e : info
2017-08-20T08:04:42.313 c10fdebd-0c7f-4514-a39a-e7420edece7e : warn
2017-08-20T08:04:42.345 c10fdebd-0c7f-4514-a39a-e7420edece7e : error
2017-08-20T08:04:42.345 Function completed (Success, Id=c10fdebd-0c7f-4514-a39a-e7420edece7e, Duration=43ms)

テーブルのほうにはPartitionKeyがIでRowKeyが、context.invocationIdの値みたいな感じで出るみたいです。

f:id:okazuki:20170820170821p:plain

これのLogOutputプロパティを見ると結果が格納されていました。

bd949c0d-f32b-4fad-99af-32e8c096df00 : verbose
bd949c0d-f32b-4fad-99af-32e8c096df00 : info
bd949c0d-f32b-4fad-99af-32e8c096df00 : warn
bd949c0d-f32b-4fad-99af-32e8c096df00 : error

ここで気になるのはエンテティの最大サイズが1MBだという点です。

エンティティ:エンティティは、プロパティのセットで、データベースの行に似ています。 エンティティの最大サイズは 1 MB です。

docs.microsoft.com

まぁ1MBを超えるようなログを出すほど大きな関数は無いだろうということなんでしょうね?

ということで、こんな関数を作ってみました。

module.exports = function (context, req) {
    for (var i = 0; i < 1024 * 1024; i++) {
        context.log('index: ', i,' - 01234567890');
    }
    context.res = {
        status: 200,
        body: "OK"
    };
    context.done();
};

多分、1MB超えると思います!

実行するとポータル上はこんな感じになりました。私もそう思います。

f:id:okazuki:20170820173656p:plain

実行が終わったのでテーブルストレージを見てみます。

f:id:okazuki:20170820173859p:plain

あ…割と早々に切り捨てられてますね。ログの出しすぎには注意しましょう。

ログのフィルタリング

そうはいっても開発中は詳細ログを出してて運用に入るとエラーログだけ出したいですよね。 ということで、ログの出力をフィルタリングできます。

host.jsonのtracingのconsoleLevelで指定します。verbose, info, warning, error及びoffが指定できます。 waring以上しか出さないようにするには以下のようにします。

{ 
    "tracing": {
        "consoleLevel": "waring"
    }
}

こうするとポータル上のログにはこう出ます。

f:id:okazuki:20170820175553p:plain

ただ、テーブルストレージのほうには関係なくはかれちゃうみたいですね。

f:id:okazuki:20170820175744p:plain

モニターから見る

まぁこれが正攻法なんでしょうが、Functionsのポータルにミニターというのがあります。これを選ぶと、関数の呼び出し単位のログを見たりできます。

f:id:okazuki:20170820175951p:plain

これもconsoleLevelは関係ないみたいというか、テーブルストレージを見やすく出してくれてるだけにも見えます。

まとめ

色々やってみましたがモニターで見るのがよさそうですね。 あとログの出力量はほどほどに。

あと、モニターのページに書かれてましたがApplication Insightsの使用をお勧めされているので、今度はそちらも見てみたいと思います。ではでは。

Azure Functions で Service Bus の Topic 使ってみたよ(1つの関数の後に複数の関数をキックしたいみたいなのに使える)

Azure Functions で1つの処理をキックしたとします。 処理をキックしたあとに後続に処理結果を渡して続きの処理をしてほしいとします。

後続が1つなら Storage Account の Queue に Message を投げ込んでおけば、後続の処理は QueueTrigger でキックされる関数で作ればOKですね。

後続が2つなら…?3つなら…?

例えば後続の処理は HttpTrigger で作っておいて以下のようにコードからキックするという手が思いつきます。 こんな関数を用意しておいて

var http = require("http");

function invokeFunction(appName, functionName, code, callback) {
    var req = http.request({
        host: `${appName}.azurewebsites.net`,
        path: `/api/${functionName}?code=${code}`,
        method: "POST",
    }, function (res) {
        callback(res);
    });
    req.end();
}

こういう感じで呼ぶイメージですね。

var appName = "myAppName";
var functionName = "myFunctionName";
var code = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

invokeFunction(appName, functionName, code, function(res) {
    console.log(res.statusCode);
});

これを後続の処理の関数の数だけやればOKと。

いやいや、後続の処理とべったり紐づいてる感じがなんかやだ。それなら別の関数じゃなくて普通に内部メソッドで呼ぶわ!っていう気にもなりますね。まぁ非常に重たい処理とかならあれかもですが…。あ、でも重たい処理だと実装によってはレスポンスがいつまでも返ってこないとかありえそうですね。とまぁ色々辛い。

Service Bus のトピックを使おう

とまぁ、こういうときに使えるサービスとしてトピックというのがあります。 要は1つメッセージ投げ込んだら定義してる受信する人たちの受け口に対してメッセージをばらまいてくれるという感じです。Functions ではトピックをサポートしてるので簡単に使うことが出来ます。

Azure 上にリソースを配置

ということでやってみます。Azure上にFunction App(と、紐づくストレージアカウント)と、Service Busを作ります。

f:id:okazuki:20170819163344p:plain

Service Bus の接続文字列の設定

共有アクセスポリシーのRootManageSharedAccessKeyからサクッと接続文字列をもらいましょう。

接続文字列は、Function Appのアプリケーションの設定のアプリ設定(not 接続文字列)に適当なキー名で追加します。

f:id:okazuki:20170819163630p:plain

ここでは、ServiceBusConnectionStringという名前にしました。

Service Bus にトピックを作成

今回は、トピックに1つのメッセージが投げ込まれたら2つの配信先に配るみたいにしたいと思います。まず、トピックを1つ作ります。

f:id:okazuki:20170819163842p:plain

トピックを作ったら、その中にサブスクリプションを2つ作ります。

f:id:okazuki:20170819164121p:plain

これで下準備は完了です。

関数を作ろう

C# でも node.js でもどっちでもいいんですが、たまたま Visual Studio 2017 が 15.3.1 の更新走ってるので node.js で行こうと思います。

func init

で初期化して

func azure functionapp fetch 関数アプリ名

で設定を吸い出します。

func new

を3回やってInvoker(HttpTrigger), Func1(ServiceBusTopicTrigger), Func2(ServiceBusToppicTrigger)を作ります。

まず、Invokerです。こいつはURL叩かれたらログを吐いてServiceBusのトピックにメッセージを投げ込みます。

Invoker/function.json

{
  "disabled": false,
  "bindings": [
    {
      "authLevel": "function",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req"
    },
    {
      "type": "http",
      "direction": "out",
      "name": "res"
    },
    {
      "name": "topic",
      "type": "serviceBus",
      "direction": "out",
      "topicName": "functopic",
      "connection": "ServiceBusConnectionString"
    }
  ]
}

Invoker/index.js

module.exports = function (context, req) {
    context.log('JavaScript HTTP trigger function processed a request.');
    context.bindings.topic = "timestamp: " + Date.now();
    context.bindings.res = {
        status: 200,
        body: "invoked",
    };
    context.done();
};

次に、トピックからメッセージを受け取った時の関数を作ります。

Func1/function.json

{
  "disabled": false,
  "bindings": [
    {
      "name": "serviceBusMessage",
      "type": "serviceBusTrigger",
      "direction": "in",
      "topicName": "functopic",
      "subscriptionName": "func1",
      "connection": "ServiceBusConnectionString"
    }
  ]
}

Func1/index.js

module.exports = function(context, serviceBusMessage) {
    context.log('Func1 called.', serviceBusMessage);
    context.done();
};

Func2も同じ要領で(function.jsonに指定しているtopicNameが違うだけ)

Func2/function.json

{
  "disabled": false,
  "bindings": [
    {
      "name": "serviceBusMessage",
      "type": "serviceBusTrigger",
      "direction": "in",
      "topicName": "functopic",
      "subscriptionName": "func2",
      "connection": "ServiceBusConnectionString"
    }
  ]
}

Func2/index.js

module.exports = function(context, serviceBusMessage) {
    context.log('Func2 called.', serviceBusMessage);
    context.done();
};

ここまで出来たら、以下のコマンドでデプロイしましょう。

func azure functionapp publish 関数アプリ名

動作確認

ポータルから Invoker 関数のURLを取得します。

f:id:okazuki:20170819165225p:plain

そして、関数が実行されたか確認するためにアプリログを有効にしてログストリーミングを開いておきます。ログを確認できる状態にしたら、PostmanとかブラウザでURLをたたいてみましょう。

f:id:okazuki:20170819165518p:plain

ログを見ると、ちゃんとInvokerが呼ばれた後にFunc1とFunc2が呼び出されていることが確認できます。

2017-08-19T07:58:03.375 Executing HTTP request: {
  "requestId": "6e79f229-a593-424c-89de-1c15f03ffce1",
  "method": "GET",
  "uri": "/api/Invoker"
}
2017-08-19T07:58:03.375 Function started (Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.375 Executing 'Functions.Invoker' (Reason='This function was programmatically called via the host APIs.', Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.375 JavaScript HTTP trigger function processed a request.
2017-08-19T07:58:03.375 Function started (Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.375 JavaScript HTTP trigger function processed a request.
2017-08-19T07:58:03.640 Function completed (Success, Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35, Duration=268ms)
2017-08-19T07:58:03.640 Function started (Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)
2017-08-19T07:58:03.655 Func1 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Function completed (Success, Id=c29a7668-b9a9-4e49-af13-68e4f43a8747, Duration=4ms)
2017-08-19T07:58:03.640 Function started (Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.655 Func2 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Function completed (Success, Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1, Duration=4ms)
2017-08-19T07:58:03.640 Function completed (Success, Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35, Duration=268ms)
2017-08-19T07:58:03.640 Executed 'Functions.Invoker' (Succeeded, Id=7d545c0c-0d48-48e4-8b2e-7617b7ac9c35)
2017-08-19T07:58:03.640 Executed HTTP request: {
  "requestId": "6e79f229-a593-424c-89de-1c15f03ffce1",
  "method": "GET",
  "uri": "/api/Invoker",
  "authorizationLevel": "Function"
}
2017-08-19T07:58:03.640 Response details: {
  "requestId": "6e79f229-a593-424c-89de-1c15f03ffce1",
  "status": "OK"
}
2017-08-19T07:58:03.640 Function started (Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.640 Executing 'Functions.Func2' (Reason='New ServiceBus message detected on 'functopic/Subscriptions/func2'.', Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.640 Function started (Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)
2017-08-19T07:58:03.640 Executing 'Functions.Func1' (Reason='New ServiceBus message detected on 'functopic/Subscriptions/func1'.', Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)
2017-08-19T07:58:03.655 Func2 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Func1 called. timestamp: 1503129483375
2017-08-19T07:58:03.655 Function completed (Success, Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1, Duration=4ms)
2017-08-19T07:58:03.655 Executed 'Functions.Func2' (Succeeded, Id=7ce750a6-cbbd-4f95-baa7-3f8e68751cf1)
2017-08-19T07:58:03.655 Function completed (Success, Id=c29a7668-b9a9-4e49-af13-68e4f43a8747, Duration=4ms)
2017-08-19T07:58:03.655 Executed 'Functions.Func1' (Succeeded, Id=c29a7668-b9a9-4e49-af13-68e4f43a8747)

まぁ順番はばらっばらですけどね。仕方ないでしょう。

気になるのは、最初のほうで

JavaScript HTTP trigger function processed a request.

が2回出てるところでしょうか…なんぞこれ。あとで見てみよう。

まとめ

ということで、後続の関数が増えたらトピックでサブスクリプションを増やして、それをトリガーにする関数を追加するだけでいいというお手軽さ。すっかり忘れてたよトピック。

Azure Functions のプランによる違いメモ

2017/08/19 時点の自分用メモです。
詳細は、ドキュメントを確認してください。

https://docs.microsoft.com/ja-jp/azure/azure-functions/

Azure Functions では作成時にプランを選べます。選べるプランは2つ。

  • 従量課金プラン
  • App Service プラン

従量課金プラン

勝手にスケールしてくれるし、最低限のコストしかかからないみたいですね。 放置してても、負荷に応じてよしなにしてくれる。

従量課金プランの制限事項

関数の実行期間は最大で10分みたいです。

これも、デフォルト値は5分でhost.jsonfunctionTimeoutで指定しないと10分まで伸びないみたいですね。

BlobTriggerをつかってると10分くらい遅延することがあるみたいです。嫌だったらQueueTriggerとか使ってねってことらしい。

App Service プラン

普通のWebAppとかと同じ考えっぽい。 要は自分で指定したスペックの上のマシンで動く。

常時接続を有効にしておく(確かBasic以上から使えるんでしたっけ?)と、継続的に実行することもできるみたい。というか常時接続をONにしておかないと、App Serviceが暇になると寝ちゃうみたいで起こすにはHttpTriggerじゃないとダメということになるみたいですね。

QueueTriggerとか動かなくなるんでしょうね。怖い。

注意点

先にも書いた通り常時接続を有効にしてサーバーが寝ないようにしましょう。

node.js を使う場合は、シングルコアを使いましょう(マルチコアにしても性能上がらないみたいですね)。性能上げたいときはインスタンス数を増やしましょう。

まとめ

10分以上動かすか動かさないかってところが一番大きなインパクトなのかなぁ?

HoloLens で Managed Plugin を作ってみよう。そしてifディレクティブからの脱却。

要はUWP(HoloLens)で動くときはUWP用のDLLを使って、Editorで動くときはEditor用のDLL使おうぜってことみたいです。

作り方の説明は以下のサイトがとても参考になります。

satoshi-maemoto.hatenablog.com

プラグインを実機デバッグしたかったらこうするみたい

blog.d-yama7.com

やってみよう!

ということで VS2017 で .NET Framework 3.5 をターゲットにしたクラスライブラリプロジェクトと UWP 用のクラスライブラリプロジェクトを作ります。ここに同じインターフェースを持ったクラスを別々に作ればいいってことですね。

とりあえずプロジェクトのプロパティで生成されるアセンブリ名と規定の名前空間は同じに合わせておきました。

f:id:okazuki:20170817201626p:plain

ここに同じインターフェースを持つけど処理(というほどのことでもない)の違うクラスを作っていきます。

まずはUnityEditorで動くほう

namespace HelloWorldPlugin
{
    public class MessageProvider
    {
        public string CreateMessage(string name)
        {
            return string.Format("Hello {0}. UnityEditor version.", name);
        }
    }
}

次にHoloLensで動くほう

namespace HelloWorldPlugin
{
    public class MessageProvider
    {
        public string CreateMessage(string name) => $"Hello {name}. HoloLens version.";
    }
}

こんな簡単な処理でもレガシーなものとそうじゃないものの差が…。

Unity のプロジェクトを作って Plugins フォルダにコピーします(HoloLens用のはPlugins/WSAに)。私は手動コピーがだるかったのでReleaseビルドしたら、Unityのプロジェクトの該当フォルダに出力するように出力フォルダを変えました。

そして、Select platforms for pluginとPlatform settingsをちゃちゃっと設定します。

UnityEditor用のプラグイン(dll)に対する設定

f:id:okazuki:20170817204730p:plain

HoloLens用のプラグイン(dll)に対する設定

f:id:okazuki:20170817202820p:plain

プロジェクトの設定

HoloToolkit を入れてHoloToolkitのメニューからProject Settingします。

そして、Main Cameraを葬ってHoloToolkitからHoloLensCamera.prefabとInputManager.prefabとInteractiveMeshCursor.prefabをシーンに追加します。

動作確認コードの作成

3DTextPrefab.prefabを追加してX:0, Y:0, Z:2くらいに置きます。 C# スクリプトを PluginTestBehavior という名前で追加して 3DTextPrefab に張り付けましょう。

UWP のプロジェクトをビルド(C# Projectも出すように設定してね)で吐き出します。

プロジェクトを開いてターゲットプラットフォームをAny(なぜこれがデフォルトなのか)からx86にしましょう。 そして、PluginTestBehaviorを以下のように編集します。

using HelloWorldPlugin;
using UnityEngine;

public class PluginTestBehavior : MonoBehaviour
{
    private void Start()
    {
        var plugin = new MessageProvider();
        this.gameObject.GetComponent<TextMesh>().text = plugin.CreateMessage("okazuki");
    }
}

実行して動作確認

UnityEditor で実行してみましょう。こういう結果になるはずです。

f:id:okazuki:20170817205412p:plain

ちゃんとUnityEditorのが動いてますね。次はVisual StudioからHoloLens(私はエミュレータ)で実行してみましょう。

f:id:okazuki:20170817205826p:plain

ちゃんと HoloLens のものが動いてますね。

めんどい

同じようなコードを2つ書くのってだるいですよね。それにインターフェースに乖離が発生する可能性が高いです。 ということで少しでも軽減する方法を

共有プロジェクト

ソースコードを共有してしまおうという考えです。 ソリューションに共有プロジェクトを追加します。(HelloWorldPlugin.Sharedみたいな名前で)

そして、プロジェクトの参照設定から作成した共有プロジェクトをUnityEditor用プロジェクトとHoloLens用プロジェクトに追加します。

共有プロジェクトに、以下のようにコードを書きます。#if ディレクティブで UWP のとき(NETFX_CORE)とそうじゃないときでコードを分岐してます。

namespace HelloWorldPlugin
{
    public class MessageProvider
    {
        public string CreateMessage(string name)
        {
#if NETFX_CORE
            return $"Hello {name}. HoloLens version.";
#else
            return string.Format("Hello {0}. UnityEditor version.", name);
#endif
        }
    }
}

個別のプロジェクトにあったMessageProvider.csはさくっと消します。

これでビルドすればOKですね。ソースコードが1つになったよやったね!エディターの左上でプロジェクトを切り替えると、そのプロジェクトの条件でコンパイルされるときのコードだけが色付きで表示されて、そうじゃないやつはコメントみたいな色になるのでわかりやすいね!

この方法の嫌なところ

if 地獄はコードが見づらい。

ということで、ほかの方法も考えてみました。

partial class

C# には1つのクラスを複数ファイルで定義する方法が提供されています。 もともと自動生成コードと手で書くクラスを分離するためとかに用意されたやつですね。 これを使うとこうなります。

共有プロジェクト

namespace HelloWorldPlugin
{
    public partial class MessageProvider
    {
        public string CreateMessage(string name)
        {
            return this.CreateMessageCore(name);
        }
    }
}

UnityEditor用プロジェクト

namespace HelloWorldPlugin
{
    public partial class MessageProvider
    {
        private string CreateMessageCore(string name)
        {
            return string.Format("Hello {0}. UnityEditor version.", name);
        }
    }
}

HoloLens用プロジェクト

namespace HelloWorldPlugin
{
    public partial class MessageProvider
    {
        private string CreateMessageCore(string name) => $"Hello {name}. HoloLens version.";
    }
}

感想

一部の実装が違うときはこれでもいいかも。共有部分では、C#の最新機能使えないのがちょっと悲しいけど。

interface だけ共有

実装内容がそもそも UnityEditor と UWP でまるっきり違うときは、インターフェースの乖離がおきないように interface だけ共有プロジェクトで定義して、個別のプロジェクトでそれを実装するというアプローチもいいと思います。

共有プロジェクト

namespace HelloWorldPlugin
{
    public interface IMessageProvider
    {
        string CreateMessage(string name);
    }
}

UnityEditor用プロジェクト

namespace HelloWorldPlugin
{
    public class MessageProvider : IMessageProvider
    {
        public string CreateMessage(string name)
        {
            return string.Format("Hello {0}. UnityEditor version.", name);
        }
    }
}

HoloLens用プロジェクト

namespace HelloWorldPlugin
{
    public class MessageProvider : IMessageProvider
    {
        public string CreateMessage(string name) => $"Hello {name}. HoloLens version.";
    }
}

感想

UnityEditor 用はダミーデータを返すだけで、HoloLens用に本番ロジックをがっつり詰め込むときはこっちのほうが綺麗な気がしますね。

HoloLens でオブジェクトのないところの AirTap を検出したい

ものをAirTapしたときの処理は、前の記事でやりました。

blog.okazuki.jp

今度は何もないところをAirTapしたときの処理とか書きたくなりました。 たとえば今うつってる画面の写真とりたいとかって感じに使えるかなと思います。

やり方は簡単でIInputClickHandlerを実装したBehaviorをくっつけたゲームオブジェクトをHoloToolkit.Unity.InputModule.InputManager.Instance.PushFallbackInputHandlerに渡してやればいいみたいです。

なんでGameObject求めるんでしょうね?感覚的にはIInputClickHandlerを受け取るのが自然な気がするけどHoloというかUnityの世界の何かがあるのでしょう。

ということで前回の記事のAirTapBehaviorを以下のようにすればOKです。

using HoloToolkit.Unity.InputModule;
using UnityEngine;

public class AirTapBehavior : MonoBehaviour, IInputClickHandler
{
    private void Start()
    {
        InputManager.Instance.PushFallbackInputHandler(this.gameObject);
    }

    public void OnInputClicked(InputClickedEventData eventData)
    {
        this.gameObject.GetComponent<Renderer>().material.color = Color.red;
    }
}

実行!

こうやって明らかにCubeからカーソルが外れてるであろうところを見た状態にして…

f:id:okazuki:20170816234026p:plain

ぽちっとな

f:id:okazuki:20170816234100p:plain

できた!

多分普通は見えないGameObject作って、そこにBehaviorくっつけてって感じでやる風になりそうな気がするなぁ。