LoginSignup
15
4

More than 3 years have passed since last update.

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

15
4
0

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
  3. You can use dark theme
What you can do with signing up
15
4