今回の投稿はいままでと異なり、私自身がちょっと気になったことを、意味なく深堀りしてしまう、マニアック調査の自分メモ をお届けします。ほとんどの方にとって、わりとどーでも良いこと、がネタになりますので、興味がない方はサクっとスルーお願いします。
今回のネタ
今回のネタは「ダッシュボードのWeb表示はどんな仕組みで動いているのか」です。ダッシュボードのWeb画面を対象に、フロントエンドエンジニア的っぽい観点から、どんなライブラリを使ってどう構築してあるのか、理解していきたいとおもいます。
表示側の仕組みが理解できれば、HTML5的な工夫で、フロント側からもアプリをより改善できるようになる、ことが期待できそうです。
とりあえずの調査対象
先日、ダッシュボードを使った簡単な翻訳アプリ を作成しましたので、これをWeb技術者の観点から理解していきましょう。
翻訳アプリのフローは以下のような感じで、
翻訳アプリのWeb画面は以下のような感じです。
英語のテキスト入力欄に英文を入力していくと、リアルタイムで下に日本語が出てきます。単なる静的なWebページにおける <form> タグとか <input> タグでは実現できないページですよね。
このアプリを支えているWebフロントエンド技術を見ていきましょう。
まずは古典的な一歩
Chrome や Firefox などにある開発ツール、ひと昔前まではこれだけでWebページの構成をほぼ理解できました。しかし今は、事前処理でファイルのマージなどがフツーになっているので、気軽な解析が難しくなっている気がします。
今回のアプリも見事に事前処理がされていますね。
ただし、幸いなことに難読化(min化、スペース削減)はされていないようで、ソースを読んだり、デバッガで動作を追うことは可能ですかね。
オープンソースだから
幸いなことに node-red-dashboard は GitHub で管理されるオープンソースソフトウェアなので、元のソースコードに容易にアクセス可能です。良い時代ですね。
Node-RED は Node.js ベースの環境ですから、まずは定番、package.json を眺めてみましょう。なお今は 2018年11月4日で、node-red-dashboard のバージョンは 2.10.2-beta です。
最初に確認するのは 69行目の dependencies です。
"dependencies": {
"serve-static": "~1.13.2",
"socket.io": "^2.1.1"
},
serve-static は簡易Webサーバー的なミドルウェアですね。socket.io はソケット通信ライブラリで「リアルタイム双方向イベントベースの通信を可能に」するもの、だそうです。
次に確認するのは開発用の 73行目の devDependencies です。事前処理に使用する gulp 関連のモジュールを除外したのが以下のリスト。
"devDependencies": {
"angular": "~1.5.11",
"angular-animate": "~1.5.11",
"angular-aria": "~1.5.11",
"angular-chart.js": "^1.1.1",
"angular-material": "~1.1.10",
"angular-material-icons": "^0.7.1",
"angular-messages": "~1.5.11",
"angular-mocks": "~1.5.11",
"angular-route": "~1.5.11",
"angular-sanitize": "~1.5.11",
"angular-touch": "~1.5.11",
"angularjs-color-picker": "^3.4.8",
"chart.js": "~2.3.0",
"d3": "^3.5.17",
"font-awesome": "^4.7.0",
"jquery": "^3.3.1",
"jshint": "^2.9.6",
"justgage": "^1.2.2",
"less": "~3.8.1",
"moment": "~2.22.2",
"sprintf-js": "^1.0.3",
"svg-morpheus": "^0.3.0",
"tinycolor2": "^1.4.1",
"weather-icons-lite": "^1.0.0"
}
Node-RED ダッシュボードは、基本的に jQuery + AngularJS ベースのWebアプリであることがわかります。他に使っているライブラリは以下のもの。
- chart.js: キャンバス要素を使用したシンプルなHTML5チャート
- d3 (Data-Driven Documents): Web標準を使用してデータを視覚化するためのライブラリ
- font-awesome: アイコン的なフォントとCSSフレームワーク
- jshint: JavaScript用静的コード解析ツール
- justgage: きれいなダッシュボードゲージを生成し、アニメーション化するためのプラグイン
- less: JavaScriptの公式で安定したLessのバージョン
- moment: 日付の解析、検証、操作、書式設定のための軽量の日付ライブラリ
- sprintf-js: オープンソースの sprintf 実装
- svg-morpheus: SVGアイコンのアニメーション(マテリアルデザインのDelightful Detailsトランジション)
- tinycolor2: 色の操作と変換のための小さくて速いライブラリ
- weather-icons-lite: 57の天気アイコンのコレクション
ソースページを見てみよう
だいたいの構成要素を理解できたところで、実際のページソースを見てみましょう。
今回は gulp で事前処理をしているので gulpfile.js を参照します。処理としてはわりと普通に感じますが、fixfa.js で利用ライブラリにパッチ当てているあたり興味深いですね。
97行目 のjsファイルの処理や、121行目 のcssファイルの処理が gulp.src('src/index.html') を起点としていることから、src/index.html ファイルが元となるページソースだとおもわれます。
特に以下の部分がHTMLの主体のようです。
<body ng-app="ui"
ng-controller="MainController as main"
ng-cloak layout="column"
style="background: {{main.backgroundColor}}"
class="nr-dashboard-theme"
ng-swipe-right="onSwipeRight();"
ng-swipe-left="onSwipeLeft();">
<md-content ng-if="main.loaded" ng-include="'partials/main.html'" layout="column" flex></md-content>
<div ng-if="main.nothing" class="node-red-ui--notabs">
<table><tr><td><center><img src="icon120x120.png"/></center></td></tr>
<tr><td><center><h2>Welcome to the Node-RED Dashboard</h2></center></td></tr>
<tr><td><center>Please add some UI nodes to your flow and redeploy.</center></td></tr></table>
</div>
<div ng-if="main.nothing"> タグに、ダッシュボード用のノードが定義されていない時に表示されていた以下のページの表示内容が記載されていますね。
そしてノードが定義されている場合は、AngularJS の src/partials/main.html テンプレートの内容が表示されることがわかります。現在のバージョンだと以下のようなコードです。
<md-sidenav ng-hide="main.len <= 1" layout="column" class="md-sidenav-left" md-component-id="left">
<md-list>
<md-list-item ng-repeat="obj in main.menu" ng-click="main.open(obj, $index)">
<ui-icon icon="{{obj.icon}}" style="margin:5px"></ui-icon>
<p>{{main.getMenuName(obj)}}</p>
</md-list-item>
</md-list>
</md-sidenav>
<md-toolbar ng-show="main.hideToolbar !== true" id="toolbar">
<div class="md-toolbar-tools" id="nr-dashboard-toolbar">
<md-button ng-show="main.len > 1" ng-click="main.toggleSidenav()" class="md-icon-button" aria-label="menu button">
<ng-md-icon icon="menu"></ng-md-icon>
</md-button>
<h1 ng-show="main.len <= 1" style="width:30px"> </h1>
<h1 ng-show="main.loaded">{{main.selectedTab.header || main.selectedTab.name}}</h1>
</div>
</md-toolbar>
<md-content flex>
<div ui-masonry ng-show="main.selectedTab.header">
<ui-card-panel ng-repeat="group in main.selectedTab.items" id="{{main.selectedTab.header+'_'+group.header.name | spaceToUnderscore}}" ng-if="group.header && group.header.config && !group.header.config.hidden">
<ui-component ng-repeat="control in group.items" item="control"></ui-component>
</ui-card-panel>
</div>
<div class="node-red-ui--inline-link" ng-show="!main.selectedTab.header">
<iframe class="iframe" ng-src="{{ main.selectedTab.link }}" allowfullscreen></iframe>
<!-- <div>The url {{ }} cannot be loaded into an iframe.</div> -->
</div>
</md-content>
<div id="nr-dashboard-footer" layout="row"></div>
これはページ全体とメインメニュー、ツールバー、タブまでを含んだ外側のフレーム定義ですね。タブの中に表示されるアプリケーションの本体は <iframe> タグの中にロードされるようです。
実際のパーツ定義はどこか
実際に表示されるパーツのテンプレートを探してみると、基本的なUIパーツは src/components/ui-component/templates にあるのがわかります。例えばUIの text ノード用のテンプレートは以下のようになっていました。
<md-card ui-card-size="{{ me.item.width }}x{{ me.item.height }}" layout="{{ me.item.layout }}" layout-align="{{ me.item.layoutAlign }}" class="nr-dashboard-text" ng-class="{'nr-dashboard-disabled':me.item.disabled}">
<p class="label" ng-bind-html="me.item.getLabel()"></p>
<p class="value" ng-bind-html="me.item.getText()"></p>
</md-card>
これに対応したcssテンプレートは以下です。
.nr-dashboard-text {
padding: 0 12px;
}
.nr-dashboard-text p {
margin-top: 0.1em;
margin-bottom: 0.1em;
margin-left: 0.25em;
margin-right: 0.25em;
}
.nr-dashboard-text .value {
font-weight: bold;
}
より詳細なAngularJSテンプレート構成について
と、全体のテンプレートと、text ノード用のテンプレートを参照しましたが、ここで全体像を把握してみましょう。いろんな方法があるとおもいますが、私はブラウザの開発ツールで DOM を観察するのが好きです。以下のような感じ。
細かくて見えない場合はクリックして拡大してください。
上の赤枠がページ全体を定義した src/partials/main.html テンプレートの <md-content> タグです。その子要素である下の赤枠が、text ノード用の src/components/ui-component/templates/text.html テンプレートです。
main から text まで親子関係を辿ると、間に src/components/ui-card-panel/ui-masonry.js という並びを管理している感じのロジックや、src/components/ui-card-panel/ui-card-panel.html というカード形式のパネルを表示するテンプレートなどがあります。
と、AngularJS 上の構成を把握できました。これ以上、各テンプレートの中身に踏み込んでいくと AngularJS 自体の学習になってしまいそうなので、このへんで止めておきます。AngularJS に興味のある方は、更に深く潜ってみてください!
text input ノードに関して
と、ここで終わろうと思いましたが、以下の点の調査がまだでした。
英語のテキスト入力欄に英文を入力していくと、リアルタイムで下に日本語が出てきます。単なる静的なWebページにおける <form> タグとか <input> タグでは実現できないページですよね。
text input ノードのテンプレートも見てみましょう。
<md-card ui-card-size="{{ me.item.width }}x{{ me.item.height }}" layout="row" layout-align="space-between center" class="nr-dashboard-textinput" ng-class="{'nr-dashboard-disabled':me.item.disabled}">
<md-input-container class="md-block" flex md-is-error="false" ng-class="{'has-label':me.item.label}">
<label ng-bind-html="me.item.getLabel()"></label>
<input ng-model="me.item.value"
ng-model-options="{'timezone':'UTC'}"
ng-change="me.valueChanged({{me.item.delay}})"
aria-label="{{me.item.label}}"
type="{{me.item.mode}}"
style="z-index:1"
step="any"/>
</md-input-container>
</md-card>
キー入力に対する AngularJS 的な定義 ng-change がありますね。ここでキー入力を捕まえて、バックエンドの Node-RED フローに流しているわけです。
ここで呼ばれている valueChanged 関数は src/components/ui-component/ui-component-ctrl.js の268行目 で定義されていました。こんな感じ。
me.valueChanged = function (throttleTime) {
throttle({ id:me.item.id, value:me.item.value },
typeof throttleTime === "number" ? throttleTime : 10);
};
// 中略…
var timer;
var throttle = function (data, timeout) {
if (timeout === 0) {
events.emit(data);
return;
}
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function () {
timer = undefined;
events.emit(data);
}, timeout);
};
events は UiEvents で、その関数 emit は src/services/events.js の7行目 で定義されていました。こんな感じ。
var socket = io({path:location.pathname + 'socket.io'});
this.emit = function(event, msg) {
if (typeof msg === 'undefined') {
msg = event;
event = updateValueEventName;
}
msg.socketid = socket.id;
socket.emit(event, msg);
};
ここで Socket.IO 経由で Node-RED 側と通信しているわけですね。ふう、これでやっと全体の構成と、フローの流れが理解できた気がします。
おわりに
と、自分なりに調査して、Node-RED ダッシュボードの Web 側の構成と仕組み、おおまかな動作フローを理解できました。ここまで理解できると、今後のダッシュボード利用の際に、柔軟な対応ができそうな気がします。
今回のリポジトリ、node-red-dashboard を深く理解するには gulp と AngularJS の知識が不可欠のようです。逆に考えれば、これらを学べる良い題材かもしれませんよ!
今回は、いつもより更に独りよがりなコンテンツで、わかりにくい文章だとおもいます。すみません。もしここまで根気強く読んでくださった方が居たとして、なにかわかりにくいこと、不明なことなどありましたらコメントをいただけますと嬉しいです。可能であれば追記・追加調査しますので!
とりあえず、調査でいろんなことが学べるのは楽しいです!もっと余裕が欲しい…
それではまた!