node.jsでPromiseを使って同期的に処理を行う

node.jsでPromiseを使って同期的に処理を行う

前回の補足的なエントリーです。

uepon.hatenadiary.com

以前のgoogle-home-notifierを使用した処理では以下のようなコードを書いてみました。 いちいちプログラムを起動するのも面倒だなと思ったので、常に入力を受け付けるような処理にしたいと思っていました。 ただ、キーボード入力と非同期実行のお陰で単純に書いても予想通りには動きません。(node.jsではこの部分が個人的には一番ハードルが高いと思う)

var googlehome = require('google-home-notifier');
var language = 'ja'; // if not set 'us' language will be used

googlehome.device('テスト', language);
googlehome.ip('xxx.xxx.xxx.xxx');  //IPアドレスは自分も持っているデバイスを調べて入力してください

var text = 'こんにちは';

if(process.argv.length == 3){
        text = process.argv[2];
}

try {
        googlehome.notify(text, function(notifyRes) {
                console.log(notifyRes);
        });
} catch(err) {
        console.log(err);
}

このコードではコマンド引数に与えた文字列を単純にgoogle-home-notifierモジュールのnotifierメソッドに渡しています。 この処理を無限ループにすることでいいはずなのですが、文字入力が完了する前に次の処理がおこなわれるので文字には空文字列が入り、 目的とは違った空振りのコードで動いてしまします。node.jsではI/O処理も非同期的に動作するため、時間のかかる処理では データが格納される前に次の処理が行われてしまいます。ネットワークからのデータ取得やファイルからの読み込み・書き込み、キーボード入力などが該当します。

そういう場合にはpromiseを使うようです。くっそ適当に言ってます。 C#みたいなasync/awaitの方が理解しやすいんですが、node.jsのasync/awaitは少し違っているみたいに思ったので、 今回はpromiseを理解することにしました。

const readlineSync = require('readline-sync');

var googlehome = require('google-home-notifier');
var language = 'ja';
googlehome.device('テスト', language);
googlehome.ip('xxx.xxx.xxx.xxx');

console.log('話させたい文字列を入力してください>');
process.stdin.setEncoding('utf-8');
process.stdin.on('data', function (data) {
        googleHomeNotify(data).then(()=> {
                return PromiseSleep(2000);
        }).then(() => {
                console.log('話させたい文字列を入力してください>');
        });
});

function googleHomeNotify(data) {
        return new Promise((resolve, reject) => {
                try {
                        googlehome.notify(data, function(notifyRes) {
                                console.log(notifyRes);
                        });
                } catch(err) {
                        console.log(err);
                }
                resolve();
        });
}

function PromiseSleep(time) {
        return new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve();
                }, time);
        });
}

基本的な流れとしては

  1. 常時入力待機(イベント)
  2. 入力が行われたらPromiseを生成して、その中でgoogle-home-notifierのnotifyメソッドに入力された文字列を引数に実行
  3. google-home-notifierのnotify処理を行っている間にwait処理を行う(setTimeoutメソッド)
  4. wait時間が過ぎたら次の文字列入力を促す表示を行う
  5. 入力待機状態に戻る

こんな感じです。process.stdin.on(...)の部分をwhileやforなどで無限ループにするとうまく動きません。Promiseと同期型のループ処理で散々悩みました…(単体では動くがループでは動かない)

もっと、多分良い書き方があるのかなと思いますが、こんな感じでかけました。もう少しスッキリかけるといいなあというところです。

関数の処理を見直して以下のように書き直してみました。

const readlineSync = require('readline-sync');

var googlehome = require('google-home-notifier');
var language = 'ja';
googlehome.device('テスト', language);
googlehome.ip('xxx.xxx.xxx.xxx');

console.log('話させたい文字列を入力してください>');
process.stdin.setEncoding('utf-8');
process.stdin.on('data', function (data) {
        googleHomeNotify(data).then(()=> {
                return PromiseSleep(2000);
        }).then(() => {
                console.log('話させたい文字列を入力してください>');
        });
});

function googleHomeNotify(data) {
        return new Promise((resolve, reject) => {
                try {
                        googlehome.notify(data, function(notifyRes) {
                                console.log(notifyRes);
                        });
                } catch(err) {
                        console.log(err);
                }
                resolve();
        });
}

function PromiseSleep(time) {
        return new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve();
                }, time);
        });
}

まとめ

いままでjavascriptの非同期的な部分は避けて通ってきましたが、やっと重い腰を上げられたかなと思います。

さらにPromiseとasync/awaitを使ってわかりやすくしてみました。 ネストが少し浅くなったのでわかりやすくなった気がします。

const readlineSync = require('readline-sync');

var googlehome = require('google-home-notifier');
var language = 'ja';
googlehome.device('テスト', language);
googlehome.ip('xxx.xxx.xxx.xxx');

console.log('話させたい文字列を入力してください>');
process.stdin.setEncoding('utf-8');
process.stdin.on('data', async function (data) {
        googleHomeNotify(data);
        await PromiseSleep(3000);
        console.log('話させたい文字列を入力してください>');
});

function googleHomeNotify(data) {
        try {
                googlehome.notify(data, function(notifyRes) {
                        console.log(notifyRes);
                });
        } catch(err) {
                console.log(err);
        }

function PromiseSleep(time) {
        return new Promise((resolve, reject) => {
                setTimeout(() => {
                        resolve();
                }, time);
        });
}

これで非同期的に動作する機能をわかりやすく記述することができたような気がしますね。