自分メモを兼ねた Node-RED で遊んでみた的な、3投稿目。今回は function ノードを中心に試してみたいとおもいます。
資料としてはユーザー会の Functionノードの書き方 が非常にわかりやすいので、ここに記載された内容を自分で試してみる感じになるとおもわれます。環境は Node-RED v0.19.2です。
今回のサンプル
前回の配列の値を3倍するサンプル を今回もそのまま使います。意地悪して遊ぶ前の、普通に動作していた時のもの。
以下の JSON をクリップボード経由で読み込んでもokです。
[{"id":"1c739a91.17b5d5","type":"inject","z":"aba7fcae.18f7e","name":"1-5","topic":"","payload":"[1,2,3,4,5]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":40,"wires":[["2a538d6c.2ab1b2"]]},{"id":"446cc941.b78e18","type":"debug","z":"aba7fcae.18f7e","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":670,"y":40,"wires":[]},{"id":"2a538d6c.2ab1b2","type":"split","z":"aba7fcae.18f7e","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":230,"y":40,"wires":[["8f29f263.96a27"]]},{"id":"8f29f263.96a27","type":"function","z":"aba7fcae.18f7e","name":"* 3","func":"msg.payload *= 3;\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":40,"wires":[["87108160.8182a"]]},{"id":"87108160.8182a","type":"join","z":"aba7fcae.18f7e","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":510,"y":40,"wires":[["446cc941.b78e18"]]}]
テスト用のfunctionノードを追加する
サンプルにある function ノードは複数回呼ばれるので、join ノードの後にもうひとつの function ノードを追加しておきましょう。コードはそのままで名前を「Test」にします。
この状態でデプロイして実行すると、いま追加した「Test」ノードには特に処理が追加されていないため、前回と同じ要素が5個の配列が出力されるはずです。
変数の共有範囲と値の保持
さて、コードを書く上でまず気になるのは、変数などの有効範囲です。ローカル変数、とかグローバル変数、とかいうアレですね。
Node-RED の function ノードを作成する場合、以下のような4レベルの共有範囲があるようです。
名前 | 有効な範囲 | 値の保持 |
---|---|---|
ローカル Local |
各functionノード | その処理だけの一時的なもの |
コンテキスト Context |
各functionノード | 各functionノードが保持 |
フロー Flow |
各フロー | 各フローが保持 |
グローバル Global |
全フロー | Node-RED実行環境 が保持 |
それぞれの変数を作成してみる
まずは「Text」ノードのコード欄に、以下のような各値の出力ロジックを記載します。
var localValue = 0;
var contextValue = context.get('test-value')||0;
var flowValue = flow.get('test-value')||0;
var globalValue = global.get('test-value')||0;
msg.payload = {
local: localValue,
context: contextValue,
flow: flowValue,
global: globalValue
}
return msg;
デプロイしてinjectノードをクリックすると、当然ながら以下のように全て 0 の値が出力されます。
なお context.get() 関数などの後にある ||0 は、指定したキー 'test-value' がまだ未定義だった場合に、デフォルト値として 0 を使用するという、JavaScript でよく使用される記述法です。動作としては以下と同じです。
var contextValue = context.get('test-value');
if (!contextValue) {
contextValue = 0;
}
値を加算してみる
先程のコードだとずっと 0 が表示されるままなので、それぞれの値に 1 を加算してみると、以下のようなロジックになります。
var localValue = 0;
var contextValue = context.get('test-value')||0;
var flowValue = flow.get('test-value')||0;
var globalValue = global.get('test-value')||0;
localValue++;
contextValue++;
flowValue++;
globalValue++;
context.set('test-value', contextValue);
flow.set('test-value', flowValue);
global.set('test-value', globalValue);
msg.payload = {
local: localValue,
context: contextValue,
flow: flowValue,
global: globalValue
}
return msg;
あれ、急に長くなったな?と感じますね。
ローカル変数である localValue を除いた値は、それぞれのオブジェクトが保持して管理しています。これらの値を参照するだけなら良いのですが、値を書き換えた場合はそれを set() 関数で反映する必要があります。
さてデプロイして、injectノードをクリックしてみましょう。以下は、3回クリックした結果になります。
どうでしたか、予想通りの結果でしょうか?
ローカル変数である localValue は常に 1 であるのに対し、それ以外の変数は前の値を維持しているのがわかります。
グローバル変数を理解する
さて、これらの動作を別のフローで実行するとどうなるでしょうか?
今のフローをコピー…する機能はなさそうなので、とりあえず フローの新規追加 で、新しく空っぽのフローを作成してください。
そして現在のサンプルフローをメニューの「書き出し -> クリップボード」からJSON形式でコピーし、「読み出し -> クリップボード」で新しいフローにバシッと貼り付けてください。
以下の JSON データを貼り付けてもいいですよ。
[{"id":"1c739a91.17b5d5","type":"inject","z":"aba7fcae.18f7e","name":"1-5","topic":"","payload":"[1,2,3,4,5]","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":90,"y":40,"wires":[["2a538d6c.2ab1b2"]]},{"id":"446cc941.b78e18","type":"debug","z":"aba7fcae.18f7e","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":810,"y":40,"wires":[]},{"id":"2a538d6c.2ab1b2","type":"split","z":"aba7fcae.18f7e","name":"","splt":"\\n","spltType":"str","arraySplt":1,"arraySpltType":"len","stream":false,"addname":"","x":230,"y":40,"wires":[["8f29f263.96a27"]]},{"id":"8f29f263.96a27","type":"function","z":"aba7fcae.18f7e","name":"* 3","func":"msg.payload *= 3;\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":40,"wires":[["87108160.8182a"]]},{"id":"87108160.8182a","type":"join","z":"aba7fcae.18f7e","name":"","mode":"auto","build":"string","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":"false","timeout":"","count":"","reduceRight":false,"x":510,"y":40,"wires":[["335f7246.29f6fe"]]},{"id":"335f7246.29f6fe","type":"function","z":"aba7fcae.18f7e","name":"Test","func":"var localValue = 0;\nvar contextValue = context.get('test-value')||0;\nvar flowValue = flow.get('test-value')||0;\nvar globalValue = global.get('test-value')||0;\n\nlocalValue++;\ncontextValue++;\nflowValue++;\nglobalValue++;\n\ncontext.set('test-value', contextValue);\nflow.set('test-value', flowValue);\nglobal.set('test-value', globalValue);\n\nmsg.payload = {\n local: localValue,\n context: contextValue,\n flow: flowValue,\n global: globalValue\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":40,"wires":[["446cc941.b78e18"]]}]
これで先程のサンプルフローがコピーされ、同じフローが2つ用意されたはずです。
この状態で、新しく作成されたコピーのほうのフローで、デプロイして、injectノードを3回クリックしてみましょう。さて、どうなるでしょうか?
どうですか、予想通りの結果になりましたか?
さきほどと同じに見えますが、global 値だけが元のフローの値を保持しているのがわかります。グローバルはNode-RED実行環境で保持される値、なので全てのフローがこの1つの値を共有しているわけです。
値をリセットしたい
さて、グローバル変数に関してはわかりましたので、元のフローに戻りましょう。コピーしたフローは削除してかまいません。
今後のため、ここで、それぞれの値をリセットする機能を作ってみます。
フローの空いている場所に inject ノードと functon ノード配置し、接続します。
function ノードに以下のコードを設定してください。
context.set('test-value', 0);
flow.set('test-value', 0);
global.set('test-value', 0);
return msg;
それぞれの保持している値を、0 にリセットするコードです。ちゃんと動作するでしょうか?
デプロイしてリセット機能を試した結果が以下になります。
見てすぐわかる通り、コンテキスト変数だけが初期化できていません。
この動作は、コンテキスト変数の性質を考えれば理解できます。コンテキスト変数は、ノード単位で保持される値です。カウントしている「Test」function ノードと、今回のリセット動作を行う function ノードは、function ノードという同じ種類ではありますが、フロー上では別のノードです。
フロー上にたくさんのノードがありますが、これらノードひとつひとつが保持しているのがコンテキストです。このコンテキストはノード外部からのアクセスが難しいため、より安全に値を管理できますが、今回のように外からのリセットも難しいわけです。
ではコンテキスト変数を含めて、全ての変数を初期化するにはどうしたら良いのでしょうか?それも実は簡単です。Node-RED環境を再始動させると、ある意味残念なことですが、値は全て初期値に戻ります。
なお、フローを修正してデプロイすると、そのフローに関しては再起動がかかるみたいですね。これも値のリセットに利用できるかもしれません。
(余談) データ永続化のハナシ
今回はあくまで変数のスコープ的なものをメインにしているので、いわゆるデータの永続化はあまり気にしていません。
実際にアプリを作成する際には、フロー変数やグローバル変数もあくまで一時的なものと考え、サーバーの再起動などで消えることに注意が必要です。きちんと保持しておきたい情報は、ファイルやDBなどにきちんと値を格納しておく、いわゆる永続化を考慮にいれて設計しましょう。
まあ、Node-RED でも既に、永続化の機能をもったノードとか、拡張とかが作られていそうな気もしますが。そのうち探してみたいと思います!
フロー変数を理解する
では、更に変更して試してみましょう。以前からある「* 3」function ノードですが、こちらのコードも上記の「Test」用のコードで置き替えます。こんな感じで。
全体のフローは以下で、2つある function ノードの中のコードは同じです。ただし debug ノードに直結しているのは「Test」の function ノードだけだということはお忘れなく!
これをデプロイし、Node-RED環境を再起動して初期値を 0 にリセットしたうえで実行すると、結果はどんな風になるでしょうか?1回の実行で「* 3」のノードは5回動作することを忘れないでください。
結果は以下のようになりました。予想通りでしたか?
順に答え合わせをしていきましょう。
まずローカルですが、これは自身の過去の実行を含め、他からの影響をまったく受けないので常に 1 を出力しています。
次にコンテキストですが、これは「Test」function ノードのコンテキストに格納された値なので、1回の実行で1回動作し、結果として値が1増加する動きになります。ちなみに「* 3」のコンテキストにも同様の値がありますが、こちらは1回の実行で5回の呼び出しがあり、もし表示されていれば5, 10, 15 という値だったと想像できます。しかし「* 3」function ノードの出力は利用されていない (join で配列に変換されるものの、「Test」function ノードが受け取らない) ので今回の出力には関係ありません。
【ヒント】上記を確認するには、join ノードの後に debug ノードを追加してみましょう
次にフローですが、これはこのフロー全体で共有する値なので、「* 3」function ノードの動作でも、「Test」function ノードの動作でも、同じ値が増加していきます。1回の実行で合計6回の動作が発生するので、結果は 6 ずつ増加することになります。
最後にグローバルですが、これも環境全体で共有する値なので、フローと同様の増加になります。
以上、最初に見たときは複雑に感じるかもしれませんが、それぞれの値は何が管理しているのか、よってどの範囲で有効なのか。そこを区別して考えていけば自然と慣れていくとおもわれます。
おわりに
4種類の変数を試すだけで、第三歩の投稿が終わってしまいました。
あまりの進行の遅さにびっくりしますが、まあ、締め切りがあるわけじゃなし。自分の気のすむまで、いろいろ試して納得していきたいと思っています。
という自分なりのメモ的な資料ですので、皆様のお役に立つかどうかは甚だ疑問ではありますが。。ごく少数の方でも、何かを理解するヒント程度にさえなってくれれば、公開した甲斐はあったというものです。
ではでは。