株式会社日立製作所 研究開発グループ
サービスコンピューティング研究部
西山博泰
はじめに
プログラミングの手間を劇的に削減しアプリケーション開発者の裾野を広げる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ノードを含む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
入力項目でwidth
とheight
を設定します。
次の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を有効化するため、ノード設定画面の初期化を行う関数を定義するoneditprepare
でelementSizer
関数を呼び出して初期化を行います。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部品作成に取り組んでみてはいかがでしょうか。