search
LoginSignup
4

More than 1 year has passed since last update.

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

by HiroyasuNishiyama
1 / 2

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

はじめに

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

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

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

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

 この記事では、最近筆者とIBM開発者が作成したUI部品であるIFrameノードを例にとり、Node-RED Dashboard向けUI部品の作り方を解説したいと思います。

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

IFrameノードの構成

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

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

IFrameノードの使用例

 IFrameノードを含むNode-REDプロジェクト公式追加UIノードはGitHub上のGitHub - node-red/node-red-ui-nodes: Additional nodes for Node-RED Dashboardで公開されています。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プロパティの定義です。

           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入力項目でwidthheightを設定します。

IFrameノードの設定UI

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

    <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

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

            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_で始めなければなりません。

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

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

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

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

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を取得します。

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

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

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

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

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

    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コードのベースとすると良いでしょう。

<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コードを記述しています。

    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と呼ばれるAPIが定義されています。例えば、YouTubeではIFrameで埋め込んだ動画プレイヤーを制御するために、Web Messaging APIを用いたYouTube IFrame Player APIを定義しています。

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

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

    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ページにメッセージを送付します。

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

        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部品作成に取り組んでみてはいかがでしょうか。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
4