Node-RED Dashboard用UI部品ノードの作り方

株式会社日立製作所 研究開発グループ
サービスコンピューティング研究部
西山博泰

# はじめに

　プログラミングの手間を劇的に削減しアプリケーション開発者の裾野を広げるNo CodeやLow Codeといったキーワードが注目を集めています。[Node-RED](https://nodered.org/)はコーディングレスでプログラムを作成できるLow Codeプログラミングツールであり、IoT分野を筆頭に多様な分野で活用されています。

　Node-REDでは、ノードと呼ばれる機能ブロックををつなぎ合わせることで、とても簡単にプログラムを作成することができます。こういったノードには、乱数生成など簡単なユーティリティノードから、SNSからの情報収集、今流行りの機械学習を行うためのノードなど様々なノードが存在し、それらはOSSノードとして[多数公開](https://flows.nodered.org/)されています。

　Node-REDプロジェクトで開発されている公式ノード群に、ノードの組み合わせでGUI画面を簡単に作成できる[Node-RED Dashobard](https://github.com/node-red/node-red-dashboard)が存在します。以前は提供された基本UI部品しか利用できませんでしたが、バージョン2.10.0で新しいUI部品を定義するための[API](https://github.com/node-red/node-red-dashboard/wiki/Creating-New-Dashboard-Widgets)を、筆者とNode-RED開発元のIBM開発者が協力し整備しました。

　このUI部品定義のためのAPIを用いることで、基本部品に加えて、UI部品を新しく作成し公開できるようになりました。リスト、テーブル、SVG図形、メディア、地図表示など、さまざまなUI部品がコミュニティで活発に作成され公開されています。

　この記事では、最近筆者とIBM開発者が作成したUI部品である[IFrameノード](https://flows.nodered.org/node/node-red-node-ui-iframe)を例にとり、Node-RED Dashboard向けUI部品の作り方を解説したいと思います。

　なお、この記事ではNode-REDノードの作り方を理解していることを想定しています。一般的なノードの作成方法については、Node-REDプロジェクトの公式ページで公開されている[Creating Nodes](https://nodered.org/docs/creating-nodes/)などを参照してください。

## IFrameノードの構成

　IFrameノードはHTMLのIFRAMEタグによるインラインフレーム機能を利用して、Node-RED Dashboardで作成したUI画面上に別のWebページの内容を埋め込むためのUIノードです。

　以下の例は、ダッシュボードのボタン部品とIFrameノードを使った簡易YouTubeプレイヤーの例です。この例では、YouTube動画の画面をIFrameノードを用いて埋め込んでいます。

![IFrameノードの使用例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546217/ac507ac9-9548-2fbe-a17f-53b8eead65d6.png)


　IFrameノードを含むNode-REDプロジェクト公式追加UIノードはGitHub上の[GitHub - node-red/node-red-ui-nodes: Additional nodes for Node-RED Dashboard](https://github.com/node-red/node-red-ui-nodes)で公開されています。IFrameノード関連のファイルは、`node-red-node-ui-iframe`の下にあります。

トップレベルのファイル及びディレクトリを以下に示します。

| No. | ファイル/ディレクトリ    | 概要               |
| --- | -------------- | ---------------- |
| 1   | LICENSE        | ライセンスファイル        |
| 2   | README.md      | 説明文              |
| 3   | examples/      | サンプルフロー          |
| 4   | figs/          | README.mdで使用する図  |
| 5   | icons/         | ノードのアイコン         |
| 6   | locales/       | 他言語対応のためのメッセージ定義 |
| 7   | package.json   | NPMパッケージの定義      |
| 8   | ui_iframe.html | ノードの設定画面定義       |
| 9   | ui_iframe.js   | ノードの実行コード定義      |

　1~9は通常のノードと変わりません。残りの2つのファイルを見ていきましょう。

## ノード設定画面 - `ui_iframe.html`

　ノード設定画面の定義を行うHTMLファイルでは、`RED.nodes.registerType`に渡すノード定義オブジェクト中の`defaults`プロパティで個別ノードの設定パラメータを定義します。以下は、IFrameノードの`defaults`プロパティの定義です。

```javascript
           defaults: {
                group: {type: 'ui_group', required:true},
                name: {value: ''},
                order: {value: 0},
                width: {
                    value: 0,
                    validate: function(v) {
                        var valid = true
                        var width = v||0;
                        var currentGroup = $('#node-input-group').val()|| this.group;
                        var groupNode = RED.nodes.node(currentGroup);
                        valid = !groupNode || +width <= +groupNode.width;
                        $("#node-input-size").toggleClass("input-error",!valid);
                        return valid;
                    }},
                height: {value: 0},
                url: {valuie: ""},
                origin: {value: "*"}
            },
```

　このうち、`group`, `order`, `width`, `height`の4つのパラメータは、UI部品ノードにおいて必ず定義しなくてはなりません。`group`はUI部品の所属グループ(固定幅の部品配置領域)を表現します。`order`はグループ内での配置順を表す数値で、画面レイアウトエディタなどが利用します。`width` および`height`はUI部品の幅と高さ、0は自動)を表します。

　`group`, `width`, `height`はUI部品の設定パネルで共通に指定すべき項目です。以下の図において、`Group`入力項目で`group`, `Size`入力項目で`width`と`height`を設定します。

![IFrameノードの設定UI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546217/0e7a6932-efe9-6716-a2a4-b27fbb1af914.png)

次のHTMLコードはその定義の抜粋です。最初のdiv要素が`group`の入力、2つ目のdiv要素が`width`および`height`の入力を行うためのフィールド定義です。このコードはテキストラベル部分以外はUI部品間で共通です。

```javascript
    <div class="form-row" id="template-row-group">
        <label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui_iframe.label.group"></span></label>
        <input type="text" id="node-input-group">
    </div>
    <div class="form-row" id="template-row-size">
        <label><i class="fa fa-object-group"></i> <span data-i18n="ui_iframe.label.size"></span></label>
        <input type="hidden" id="node-input-width">
        <input type="hidden" id="node-input-height">
        <button class="editor-button" id="node-input-size"></button>
    </div>
```

UI部品ノードでは、サイズ入力について、次の例に示すようなGUIによる部品サイズの入力をサポートします。

![サイズ入力UI](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/546217/3022ab68-ce04-e5f5-13aa-b73eb33ebff1.png)

この入力UIを有効化するため、ノード設定画面の初期化を行う関数を定義する`oneditprepare`で`elementSizer`関数を呼び出して初期化を行います。`elementSizer`には、`width`, `height`, `group`、それぞれのHTML入力項目のIDを指定します。

```javascript
            oneditprepare: function() {
                $("#node-input-size").elementSizer({
                    width: "#node-input-width",
                    height: "#node-input-height",
                    group: "#node-input-group"
                });
            },
```

以下は`RED.nodes.registerType`でノードの登録を行う部分のコードです。これは通常のノードと基本的に同じですが、一つだけ注意点があります。`registerType`の第一引数にはノードの内部名(型)を指定します。UI部品ノードでは、内部処理でUI部品ノードを識別するため、この名称を`ui_`で始めなければなりません。

```javascript
    RED.nodes.registerType("ui_iframe", mk_conf("iframe"));
```

## ノード実行コード - `ui_iframe.js`

　実行コード側では、ダッシュボード画面上のUI部品の画面動作定義をダッシュボードに登録する処理を行います。

　以下に実行コードの概要を示します。

```javascript
module.exports = function(RED) {
    var count = 0;
    function HTML(config) {
        ... HTMLコード定義 ...
        return html;
    }

    var ui = undefined;
    function IFrameNode(config) {
        try {
            var node = this;
            if (ui === undefined) {
                ui = RED.require("node-red-dashboard")(RED);
            }
            RED.nodes.createNode(this, config);
            var html = HTML(config);
            var done = ui.addWidget({
               ... 設定パラメータ ...
            });
        }
        catch (e) {
            console.log(e);
        }
        node.on("close", done);
    }
    RED.nodes.registerType('ui_iframe', IFrameNode);
};
```

　UI部品ノードでは、Node-RED Dashboardの部品ノード追加APIを使用します。以下のコードはAPIを取得するための処理です。Node-REDが使用しているノードを取得するため、標準のrequire文ではなく、Node-REDランタイムが提供する`RED.require`関数を利用します。また、Node-RED Dashboardとのロード順の問題を回避するため、ノードの初期化コードの中でAPIを取得します。

```javascript
            ui = RED.require("node-red-dashboard")(RED);
```

`RED.nodes.createNode`でノードを作成した後、HTMLコードを作成し、`ui.addWidget`でUI部品を登録します。HTMLコードと登録処理については、後ほど説明します。

```javascript
            RED.nodes.createNode(this, config);
            var html = HTML(config);
            var done = ui.addWidget({
               ... 設定パラメータ ...
            });
```

`addWidget`関数はノードの終了(`close`イベント発生)時に呼び出すべきコールバック関数を返します。ここでは、返却値のコールバックをそのまま`node.on("close", done)`で登録しています。

　HTML関数はUI部品の画面定義を生成します。IFrameノードの例では関数として定義していますが、HTMLコードを生成できれば良いだけですので、必ずしも関数として定義する必要はありません。

```javascript
    function HTML(config) {
        count++;
        var id = "nr-db-if"+count;
        var url = config.url ? config.url : "";
        var allow = "autoplay";
        var origin = config.origin ? config.origin : "*";
        var html = String.raw`
... HTMLコード ...
`;
        return html;
    }
```

  HTMLコードのコア部分はとてもシンプルです。次のように、IFRAMEタグでページ埋め込み用のHTMLコードを生成します。URLなどノード設定によって決まる箇所は、JavaScriptのテンプレートリテラル機能(${...})で文字列の埋め込みを行っています。

Node-REDダッシュボードには任意のHTMLを設定してUI動作を定義できるtemplateノード(`ui_template`)があります。UI部品の開発の際には、templateノードを用いてプロトタイプを作成して動作確認をし、それを上記のHTMLコードのベースとすると良いでしょう。

```html
<style>.nr-dashboard-ui_iframe { padding:0; }</style>
<div style="width:100%; height:100%; display:inline-block;">
    <iframe id="${id}" src="${url}" allow="${allow}" style="width:100%; height:100%; overflow:hidden; border:0">
        Failed to load Web page
    </iframe>
</div>
<script>
(function(scope) {
    ... JavaScriptコード ...
})(scope);
</script>
```

　埋め込みを記述するHTMLコードの後に、SCRIPTタグで囲んで、メッセージの処理などを行うJavaScriptコードを記述しています。

```javascript
    var iframe = document.getElementById("${id}");

    if (iframe && iframe.contentWindow) {
        iframe.contentWindow.addEventListener("message", function(e) {
            scope.send({payload: e.data});
        });
    };
```

　JavaScriptの最初のパートはIFrameで埋め込んだページからのメッセージをNode-REDのメッセージとして後続ノードへ送付する処理の定義です。HTML5ではIFRAMEタグで埋め込んだページと埋め込み元のページとの間でメッセージのやり取りを行うための[HTML5 Web Messaging](https://www.w3.org/TR/webmessaging/)と呼ばれるAPIが定義されています。例えば、YouTubeではIFrameで埋め込んだ動画プレイヤーを制御するために、Web Messaging APIを用いた[YouTube IFrame Player API](https://developers.google.com/youtube/iframe_api_reference?hl=ja)を定義しています。

　上記コードの`addEventListener`部分はWeb Messaging APIによって埋め込みページから送付されたメッセージを受け取り、`scope.send`関数によりNode-REDメッセージとして送付します。

　次の定義は、入力メッセージを受け取った場合の処理を定義しています。

```javascript
    scope.$watch("msg", function(msg) {
        if (iframe && msg) {
           if (msg.url) {
               iframe.setAttribute("src", msg.url);
           }
           if (iframe.contentWindow && msg.payload) {
                var data = JSON.stringify(msg.payload);
                iframe.contentWindow.postMessage(data, "${origin}");
           }
        }
    });
```

　`scope.$watch`関数はノードに対するメッセージが変化したかどうかを検出し、変化した場合にコールバック関数を実行します。
　メッセージが`url`プロパティを含む場合、IFRAMEタグの`src`プロパティを設定します。これにより、入力メッセージでURLを指定し、動的にページのリロードを行う処理を実現します。

　また、メッセージに`payload`プロパティが指定された場合、Web Messaging APIの`postMessage`によって埋め込まれたWebページにメッセージを送付します。

　最後に、登録処理のパラメータ詳細を見てみましょう。

```javascript
        var done = ui.addWidget({
            node: node,
            width: config.width,
            height: config.height,
            order: config.order,
            format: html,
            templateScope: "local",
            group: config.group,
            emitOnlyNewValues: false,
            forwardInputMessages: false,
            storeFrontEndInputAsState: false,
            convertBack: function (value) {
                return value;
            },
            beforeEmit: function(msg, value) {
                return { msg:msg };
            },
            beforeSend: function (msg, orig) {
                if (orig) { return orig.msg; }
            },
            initController: function($scope, events) {
            }
        });
```

　`addWidget`には登録したいUI部品の情報をパラメータオブジェクトとして渡します。パラメータを以下にまとめます。多くの用途ではこれらを変更する必要はないと思います。

| No. | 名称                          | 概要                                 |
| --- | --------------------------- | ---------------------------------- |
| 1   | `node`                      | ノードオブジェクト                          |
| 2   | `width`                     | UI部品の幅                             |
| 3   | `height`                    | UI部品の高さ                            |
| 4   | `order`                     | 部品の配置順                             |
| 5   | `format`                    | 上記のHTML文字列                         |
| 6   | `templateScope`             | 通常のUI部品では`local`を指定                |
| 7   | `group`                     | グループ                               |
| 8   | `emitOnlyNewValue`          | 通常は`false`                         |
| 9   | `forwardInputMessages`      | 通常は`false`                         |
| 10  | `storeFrontEndInputAsState` | 通常は`false`                         |
| 11  | `convertBack`               | 通常は引数をそのまま返す関数オブジェクト               |
| 12  | `beforeEmit`                | 通常は`msg`プロパティに第一引数を指定したオブジェクトを返す関数 |
| 13  | `beforeSend`                | 通常は、第二引数の`msg`プロパティを返す関数           |
| 14  | `initController`            | 通常は、何もしない空の関数                      |

# おわりに

この記事では、Node-RED Dashboard向けUI部品ノードの作り方を紹介しました。

ノードを組み合わせて簡単にUI画面を作成できるのはNode-REDの大きな特徴の一つです。今回紹介したように、いくつか覚えるべきことはありますが、Node-RED向けUI部品を作るのはそれほど難しくないため、皆さんもUI部品作成に取り組んでみてはいかがでしょうか。