Node-REDにおけるコンテキストの活用について

# Node-REDにおけるコンテキストの活用について

## Node-REDにおけるコンテキスト(Context)とは
Node-REDにおけるコンテキスト(Context)とは、Flowで受け渡すmsgオブジェクト以外の永続化機構：情報共有手段です。Node-REDでは、ノード間で情報を受け渡し場合、msgオブジェクトを使うのが一般的ですが、Flowの線の上を流れるmsgオブジェクトはFlow外には保存されず、Flowの一番最後のNodeに行くと消えてしまい、別にFlowを実行しても、msgオブジェクトは引き継がれません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/fe337a52-4565-f7b0-bdbf-c61ca8f772a8.png)

コンテキスト(Context)を使うと、オブジェクトをKey-Value形式で「永続化」することができます。
また、コンテキスト(Context)を使って、Node-REDの内外とデータのやり取りをすることができます。

## コンテキストの種類
Node-REDのコンテキストには3つの種類があり、永続化されるデータスコープが異なります。異なるスコープのコンテキストには、同じkeyに別々の値を設定することができます。

### (node)Context
単に「Context」とだけ書くと、単一Node内に永続化され、設定されたNode内でのみ値を使うことができます。
*以下、他の種類のコンテキストと区別するため「(node)Context」と表現します。*

(node)Contextを使えるのはFunction Nodeだけです。Function NodeのJavaScriptロジックはFunctionの実行が終わるとなくなってしまい、コード中に`let`や`const`で宣言した変数や定数はなくなってしまいますが、`context.set(key,value)`しておいたContextはNodeに永続化され、次回のFunction Node実行時に`Context.get(key)`で読み込むことで再利用することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/2e1faeb6-895b-8eb8-9890-2ddb879d5b2f.png "実行回数カウントするFlow例")

(node)Contextは、複数回実行されるFlow間で値を引き継ぐために利用されます。例えば、http-in NodeからのFlowでアクセスカウンターとして使われたり、定期実行されるFlowでのカウンターとして利用するなどのユースケースがあります。(node)Contextを使ったFunction nodeをコピペしても、保存するコンテキスト名の衝突を心配しなくてよくなります。

### Flow コンテキスト
Flow コンテキストのスコープは、設定したFlowタブ内です。設定したFlowタブ外のNodeからはアクセスできません。

Flow コンテキストは、Inject Node、Function Node、Switch Node、Change Node、Template Nodeなど、msgオブジェクトへ値をset/getできるNode内で使うことができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/a93f35d2-e68f-fb4e-4f7f-228a976fa4db.png "Change NodeでFlowコンテキストを設定する例")

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/509ecb70-a109-e56b-32fc-7277d284e4d0.png "Change NodeでFlowコンテキストを読み込む例")


Function Nodeでは、`flow.set(key,value)`で値をセットし、`flow.get(key)`で値を読み込むことができます。

```js
flow.set("count",30);
```

```js
let myCount = flow.get("count");
```

LinkノードでFlowタブ間をまたぐFlowを書く場合、移動前のFlowタブに設定したFlow コンテキストは移動先では使えないことに注意しましょう。

なお、`context.flow.get()` `context.flow.set()` と記載することもできます。（古い形式との互換性のための仕様）

### Global コンテキスト
Global コンテキストのスコープはNode-RED内全域です。すべてのFlowタブからのFlowで共通に使うことができます。

Global コンテキストもFlow コンテキストと同様、Inject Node、Function Node、Switch Node、Change Node、Template Nodeなど、msgオブジェクトへ値をset/getできるNode内で使うことができます。

Function Nodeでは、`global.set(key,value)`で値をセットし、`global.get(key)`で値を読み込むことができます。

```js
global.set("count",30);
```

```js
let myCount = global.get("count");
```

なお、`context.global.get()` `context.global.set()` と記載することもできます。（古い形式との互換性のための仕様）

### (node)Context / Flow コンテキスト / Global コンテキストの使い分け
コンテキストは永続化機構ですので、全域から使えるGlobal コンテキストだけ使えばいいと感じるかもしれません。しかし、Flowの書き出し/読み込みやFunction Nodeのコピペを想定した場合に、Globalコンテキストだけを使っていると、読み込み後のFlow内でコンテキストの名前が衝突してしまうことがあります。作成したFlowの再利用性を高めるために、コンテキストは最低限のスコープで使うようにしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/13252cf1-d163-a73a-cc34-0126c8071091.png)


## コンテキストブラウザ（サイドバータブ）について
設定されたContextは、FlowEditorのサイドバータブの「コンテキストデータ」で参照することができます。通常はサイドバータブのウインドウ幅が狭いので、メニューから選択する形になりますが、サイドバータブを広げることで表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/fc0bdd7d-5358-82a7-1631-a0e2920315f4.png)

Nodeでは選択中のNodeの(node)Contextが表示され、Flowでは表示中のFlowタブのFlowコンテキストの内容が表示されます。

表示中にコンテキストの値が変更されても自動的に表示更新されません。 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/08b578f8-1c6f-9e3e-a167-1430a544bbfc.png "reload") を押すことで更新されます。
メニュー内のチェックを入れておくことで、フォーカス対象が切り替わった際に表示自動更新することができます。（が使いにくい！）

またNode-RED 1.0以降では、![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/5855f136-f557-b083-c3d1-fe3cf22e2797.png "ゴミ箱") ボタンで設定されたコンテキストを削除することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/d5497d15-7141-6898-fe11-d4d251b38122.png)

## コンテキストのライフサイクル
### 初期化
コンテキストはNode-REDのプロセスメモリ内に永続化されますので、Node-RED自体の再起動時にはなくなってしまいます。FlowコンテキストやGlobalコンテキストは、Flowの「デプロイ」ではなくなりませんが、(node)Contextは「デプロイ」時に初期化されます。
起動後、設定されていないContextを参照すると、JavaScriptの`NaN`値となります。加工するときに不便ですので、`isNaN`を使い3項演算子で初期化するか、初期化Flow（初期化Injectから始まるFlow）で初期化を行いましょう。

```js

// 3項演算子を使ったflowコンテキスト count の初期化
let count = isNaN(flow.get("count")) ? 0 : flow.get("count");
count++;
flow.set("count",count);
return msg;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/fffc2079-1774-15bf-8791-f0e48a82fb06.png)
flowコンテキスト初期化Flow例（デプロイ時にFlowコンテキストが初期化されないように、switch Nodeで分岐している）

### 消去
コンテキストは、前述したようにコンテキストサイドバー操作で消去できます。また、Change Nodeで「値の削除」を指定してすると、Flowの中で削除できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/2558dfb3-1952-55e9-109e-1a27cab3afe6.png "Switch Nodeによる値の消去")

Function Node内では`undefined`をValueにsetすることで、当該Keyを持つコンテキストを消去することができます。

```js

// flowコンテキスト count の削除
flow.set("count",undefined);
```

### Key一覧の取得
Function Node内では、`flow.keys()` を使うことで、Flowコンテキストのkey一覧を配列で取得することができます。設定されているコンテキストを調べたり、コンテキストに対して一括で処理する場合に使うことができます。
なお、`global.keys()`も準備されていますが、settings.js内の設定`exportGlobalContextKeys: false,`によってデフォルト無効にされています。必要な場合はsettings.jsで`exportGlobalContextKeys: true,`とすることで`global.keys()`を使うことができます。

### バックアップ
以下のようなFlowを使ってコンテキストを定期的にファイルやデータベースに定期的にバックアップすることで、Node-REDが万が一非計画停止してしまった場合にも、コンテキストの内容を救うことができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/dd27d2b7-3496-248a-2dc0-2fa360f64b71.png "定期的にglobal.countを.node-red/count.bak にバックアップ")

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/c4d6e4e3-1eea-e31b-d811-3708b6770bda.png "起動時/デプロイ時にglobal.countがなかったら.node-red/count.bakからファイル読み込んでリストア")

<details><summary>バックアップサンプルflow</summary><div>

```
[{"id":"bafc3309.55a918","type":"change","z":"fac83ddb.89f0d","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"count","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":220,"wires":[["aebf1a35.3373"]]},{"id":"aebf1a35.3373","type":"json","z":"fac83ddb.89f0d","name":"","property":"payload","action":"str","pretty":false,"x":930,"y":220,"wires":[["dd613fe9.0d14a8"]]},{"id":"dd613fe9.0d14a8","type":"file","z":"fac83ddb.89f0d","name":"","filename":".node-red/count.bak","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"none","x":1300,"y":220,"wires":[[]]},{"id":"e01cdedd.3545f","type":"inject","z":"fac83ddb.89f0d","name":"","topic":"30秒おきバックアップ","payload":"","payloadType":"date","repeat":"30","crontab":"","once":false,"onceDelay":0.1,"x":260,"y":220,"wires":[["bafc3309.55a918"]]},{"id":"96b08ca6.ade97","type":"inject","z":"fac83ddb.89f0d","name":"","topic":"起動時リストア","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":230,"y":460,"wires":[["4da3658c.1e3214"]]},{"id":"4da3658c.1e3214","type":"switch","z":"fac83ddb.89f0d","name":"global.countチェック","property":"count","propertyType":"global","rules":[{"t":"null"}],"checkall":"true","repair":false,"outputs":1,"x":580,"y":460,"wires":[["dbaf4489.4236f8"]]},{"id":"dbaf4489.4236f8","type":"file in","z":"fac83ddb.89f0d","name":"","filename":".node-red/count.bak","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":860,"y":460,"wires":[["eec7ec57.ecf89"]]},{"id":"eec7ec57.ecf89","type":"json","z":"fac83ddb.89f0d","name":"","property":"payload","action":"obj","pretty":false,"x":1070,"y":460,"wires":[["f427a483.f714a"]]},{"id":"f427a483.f714a","type":"change","z":"fac83ddb.89f0d","name":"","rules":[{"t":"set","p":"count","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1340,"y":460,"wires":[[]]}]
```

</div></details>


## 高度な使い方
### npmモジュールのGlobal コンテキスト経由の利用
Function NodeのJavaScriptロジックは、Node-RED内では専用のインスタンス（サンドボックス）内で起動されるため、外部モジュールをimportしたりrequireしたりすることはできません。Node-REDの機能をnpmモジュール等の外部モジュールで拡張するためには、Custom Nodeを書く必要があります。
しかし、Node-REDの設定ファイルである`settings.js`内に`functionGlobalContext`プロパティを設定することで、起動時にGlobal Contextとして外部npmモジュールを読み込むことができます。
例えば、settings.jsで

```js
functionGlobalContext: {
    osModule:require('os')
},
```
と設定することで、Function Node内でnode.js標準モジュール[OS](https://nodejs.org/api/os.html)を以下のように使うことができます。

```js
const os = global.get("osModule");
msg.payload = os.loadavg();
return msg; // msg.payload=[m01,m05,m15]  システムの1, 5, 15分のロードアベレージを配列で得る
```

requireするnpmモジュールは、Node-RED実行時のnode.js標準パスにあるもの、もしくは`.node-red/node_modules`配下に導入したものを使うことができます。Node-RED内のみで使うnpmモジュールを導入する場合は、カスタムノードの導入時と同じように

```
cd .node-red
npm install モジュール名
```
として導入することで、Node-REDの環境にのみモジュールを導入することができます。


### コンテキストストア
前述のとおり、Node-REDのデフォルト設定では、コンテキストの内容はNode-REDのプロセスメモリ内に永続化されます。プロセスメモリはアクセス速度が速い反面、Node-REDプロセスの停止・再起動で内容が消去されてしまい、長期間のデータ蓄積などに使いにくいです。
Node-RED 0.19以降で有効になった「コンテキストストア」を設定することにより、Node-REDの外にコンテキストを永続化できるようになりました。

#### 利用できるコンテキストストアの種類
Node-RED 1.0時点では、コンテキストストアとして標準で`memory`と`localfilesystem`がサポートされています。[Context Store API](https://nodered.org/docs/api/context/methods/)を持つCustom Nodeをインストールすることで、コンテキストストアを追加できます。現在、Custom Nodeとして公開されているコンテキストストアは多くないですが、たとえば
[node-red-contrib-context-consul](https://flows.nodered.org/node/node-red-contrib-context-consul)を導入することで、HashiCorp社のConsulを使うことができるようです。

#### コンテキストストアの設定
設定はsettings.jsで実施します。`contextStorage`プロパティを記載することで設定できます。

```js
contextStorage: {
   default: "memoryOnly",
   memoryOnly: { module: 'memory' },
   file: { 
       module:"localfilesystem",
       config: {
           dir: '/home/pi/csfile',
           cache: 'true',
           flushInterval: 10
       }
　 }
},
```
例えば、上記のようにsettings.jsに設定すると、`file`と`memoryOnly`の2つの名称を持つコンテキストストアが設定されます。デフォルトがmemoryOnlyとなりますので、コンテキストストアを指定しない場合、`memoryOnly`が使われます。また、`file`は、`/home/pi/csfile/context`内に非同期で保存され、10秒ごとに内容が書き出されるコンテキストストアになります。定期的に出力されるファイルの構成は以下のようになります。

```
/home/pi/csfile/context
├── fac83ddb.89f0d (Flowコンテキスト)
│   └── flow.json 
└── global (Globalコンテキスト)
    └── global.json
```

個々のコンテキストは、JSON.parse形式の文字列です。

```
# more global/global.json
{
    "countfile": {
        "uptime": 3899,
        "timestamp": 1576203152309
    }
}
```

`localfilesystem`の詳しい仕様は[Local Filesystem Context Store](https://nodered.org/docs/api/context/store/localfilesystem#options)を参照してください。

#### コンテキストストアの使い分け
前述の設定のように複数のコンテキストストアを設定した場合、get/setのAPIに引数を追加することで、どちらに永続化するかを使い分けることができます。

```js
var m = global.get("datam",memoryOnly); // memoryOnlyが使われる
var f = global.get("dataf",file);       // fileが使われる
var d = global.get("datad");            // 指定しない場合、デフォルトはmemoryOnly
```

```js
var m = global.set("datam","abc", memoryOnly); // memoryOnlyにセット
var f = global.set("dataf","123",file);        // fileにセット
var d = global.set("datad","abc");             // 指定しない場合、デフォルトはmemoryOnly
```

Swich Node等のNodeでは、末尾にContext Storeを選択するメニューが表示され、選択できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/4e020e7c-f320-62fd-966c-4adfa654e783.png)


また、コンテキストブラウザでもコンテキストストアの種類が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25932/b448b8e1-9c9e-9643-c2ef-5e72ecf3a608.png)

なお、コンテキストのkeyは、コンテキストストアが異なる場合に同じものを設定することができます（別々のものとして扱われる）。ただ、間違いのもとになるので同じkeyを使わないようにすべきでしょう。

## 公式情報ほか
コンテキストに関する公式情報は
本家 https://nodered.org/docs/user-guide/context
日本語 https://nodered.jp/docs/user-guide/context
にあります。

また、Function Nodeで使えるAPIについては
https://nodered.org/docs/user-guide/writing-functions#storing-data
に情報がありますので、参考にしてください。