この記事は Node-RED Advent Calendar 2020 11日目 の記事です。
TL;DR
Node-RED のプロジェクトを、ターミナルからこんな感じで起動・切り替え・終了するようにします。
ショートカットをトリガーに動作し、インタラクティブに選択することができます。
使用するシェルスクリプトは、実装にまとめてあります。
背景・きっかけ
- IoTデバイスのアプリケーションを Node-RED で実装している
- 開発用 PC ローカルで Node-RED を起動・開発し、プロジェクト機能で管理している
- アプリケーションの数が増えてきて、管理するプロジェクトが増えてきた
- 複数のプロジェクトを扱う中で、様々な問題に直面した
- プロジェクトを簡単に爆速で切り替えて、今後の Node-RED ライフを快適にしたい!
プロジェクト機能とは
Node-REDのフローを、エディター上から Git 管理できる機能です。
プロジェクト機能を活用することで、以下のような利点があります。
- 一度開発したフローを再配布することができる
- IoT で利用する多くのデバイスへのデプロイするケースなどを効率化できる
- 変更箇所(Diff)のノードの単位で確認できるため、コードレビューが容易になる
- 欠陥を発見・修正したり、リファクタリングすることができる
- チームの共有智を醸成し、共通言語化することができる
- 可読性・保守性が高まる
プロジェクト機能の困りごと
ユーザディレクトリとルートディレクトが異なる
個人的には一番大きな問題でした。
以下のようなディレクトリが存在しているとします。
$ tree
# .
# ├── dir_a
# └── dir_b
dir_a
に移動して、コマンドラインの使い方 を参考に、ユーザディレクトリを設定して起動します。
$ cd dir_a && node-red --userDir ../dir_b &
$ tree ..
# .
# ├── a # <= カレントディレクトリ
# └── b # <= ユーザディレクトリ
# ├── flows.json
# ├── flows_cred.json
# ├── lib
# │ └── flows
# ├── package.json
# └── settings.js
ファイル(ファイル名:test
)に出力するフローを実装・実行します。
$ tree ..
# .
# ├── a # <= ルートディレクト(カレントディレクトリ)
# │ └── test
# └── b # <= ユーザディレクトリ
# ├── flows.json
# ├── flows_cred.json
# ├── lib
# │ └── flows
# ├── package.json
# └── settings.js
ルートディレクトリ(Node-RED を起動したディレクトリ)にファイルが生成されてしまいます。
設定値等を静的ファイルとしてフローとともに管理したいケースが多々あり、頭を悩ませていました。
解決するためには、Node-RED プロセスをプロジェクトディレクトリで起動しなくてはなりません。
プロジェクト切り替えがつらい
複数のプロジェクトを並行開発することもあり、プロジェクト切り替えが増えてきました。
また、Node-RED はエディタ上で確認するのが最も効率的だと思っています。
そのため、「この実装どうしてたっけ?」という確認のために、一時的なプロジェクト切り替えが頻発します。
快適な Node-RED ライフのためには、少ないキー入力で簡単にプロジェクトの切り替えが必要です。
数が増えると覚えきれなくなりそう
今後さらにプロジェクト数が増加すると、「あの開発の続きやるか、、、プロジェクト名なんだっけ?」という未来が見えてきます。
現在のプロジェクトの一覧から、対象とするプロジェクトをインタラクティブに選択したいと感じ始めました。
実現したいこと
以上をまとめると、快適な Node-RED ライフには以下が必要です。
- ルートディレクリを切り替え先のプロジェクトディレクトリに変更できる
- 少ないキー入力で切り替えられる
- プロジェクトをインタラクティブに選択できる
次から、これらの実現に要するツールを見ていき、実装に移っていきます。
必要なツール群
fzf とは
junegunn/fzf
fzf is a general-purpose command-line fuzzy finder.
インクリメンタル検索ができるツールです。
プロジェクトの一覧表示・検索・選択に利用します。
fx とは
antonmedv/fx
Command-line JSON processing tool
JSONファイルを処理するツールです。
configファイルのフィルタリング取得・更新に利用します。
事前準備
- プロジェクト機能を有効化し、複数のプロジェクトを作成する
- zsh をインストールする(以降、zsh 利用)
- fzf をインストールする
- fx をインストールする
- Node.JS をインストールする(Node-RED 使っている時点でOKなはず)
実装
JSON ファイル内キー・バリュー置換スクリプト
以下のシェルスクリプトを json-replace
として、実行権限を与えてパスが通る場所に保存します。
#! /usr/bin/env zsh
self=`basename $0`
function usage_echo () {
local _status=$?
local info="
Usage:
$self <json_file>
$self <json_file> <target_key>
$self <json_file> <target_key> <new_value>
"
echo $info
exit $_status
}
trap 'usage_echo' {1,2,3,15}
function object_assgin() {
local file=$1
local prefix=$2
local target=$3
local val=$4
local result
if echo $val | grep "{.*:.*}" 2>&1 > /dev/null; then
local result=`fx $file $prefix "{...this, $target: $val}"`
else
local result=`fx $file $prefix "{...this, $target: \"$val\"}"`
fi
if [ "$prefix" = "." ]; then
prefix=''
fi
node -e "
const fs = require('fs');
let jsonObject = JSON.parse(fs.readFileSync('$file', 'utf8'));
jsonObject$prefix = $result;
console.log(JSON.stringify(jsonObject))
" | fx .
}
if [ $# -eq 1 ]; then
local file=$1
fx $file .
read key\?"Type target key > "
local prefix=${key%.*}
if [ -z "$prefix" ]; then
prefix='.'
fi
local target=${key##*.}
fx $file $prefix
read val\?"Type changed value > "
json=`object_assgin $file $prefix $target $val` && echo $json > $file
fx $file $prefix
elif [ $# -eq 2 ]; then
local file=$1
local key=$2
if [[ ! "$TERM" =~ "^\." ]]; then
local key=".$key"
fi
local prefix=${key%.*}
if [ -z "$prefix" ]; then
prefix='.'
fi
local target=${key##*.}
fx $file $prefix
read val\?"Type changed value > "
json=`object_assgin $file $prefix $target $val` && echo $json > $file
fx $file $prefix
elif [ $# -eq 3 ]; then
local file=$1
local key=$2
if [[ ! "$TERM" =~ "^\." ]]; then
local key=".$key"
fi
local prefix=${key%.*}
if [ -z "$prefix" ]; then
prefix='.'
fi
local target=${key##*.}
local val=$3
json=`object_assgin $file $prefix $target $val` && echo $json > $file
fx $file $prefix
else
usage-echo
fi
実行結果
$ cat test.json
# {
# "key1": "val1",
# "key2": "val2"
# }
$ json-replace test.json key2 new_val
# {
# "key1": "val1",
# "key2": "new_val"
# }
$ cat test.json
# {
# "key1": "val1",
# "key2": "new_val"
# }
Node-RED プロジェクト管理スクリプト
以下のシェルスクリプトを保存します(例:nodered_pj_manager.sh
)。
#! /usr/bin/env zsh
# Node-RED project manager with fzf
nodered_active_pj() {
# 変数定義
local NODE_RED=$HOME/.node-red
local PORT=1880
local LOCAL_ROOT=$HOME/nodered_root
local CONFIG=$NODE_RED/.config.json
local PROJECTS_ROOT=$NODE_RED/projects
local QUIT_OPTION='QUIT' # 削除オプション
local LOCAL_OPTION='__LOCAL__' # ローカル起動オプション(プロジェクトではない)
local OPTIONS=($QUIT_OPTION $LOCAL_OPTION)
local ACTIVE_KEY='.projects.activeProject'
local PROJECTS_KEY='.projects.projects'
# アクティブプロジェクトの取得
local ACTIVE=`fx $CONFIG $ACTIVE_KEY`
# プロジェクトの一覧取得
local _PROJECTS="`fx $CONFIG $PROJECTS_KEY '?' | grep '["]' | \
awk -F'"' '{print $2}'`\n`printf '%s\n' $OPTIONS`"
local PROJECTS=`echo $_PROJECTS | sort`
# 終了 or ローカル or 対象プロジェクトの選択
local PROJECT=`echo $PROJECTS | fzf --prompt="Node-RED PJ(current: $ACTIVE) > " \
--preview "fx $CONFIG $PROJECTS_KEY.{} && \
tree -C $PROJECTS_ROOT/{} -L 1 && git -C $PROJECTS_ROOT/{} log | head -50"`
zle reset-prompt
if [ -n "$PROJECT" ]; then
if [ $PROJECT = $QUIT_OPTION ]; then # 終了選択時
# 起動中の Node-RED プロセスの一覧取得
local candidate=`ps | grep node-red | grep -v grep | awk '{print $NF}'`
# 終了する Node-RED プロセスの選択
local quit=`echo $candidate | fzf --prompt="Node-RED PJ(current: $ACTIVE) > " \
--preview "fx $CONFIG $PROJECTS_KEY.{} && \
tree -C $PROJECTS_ROOT/{} -L 1 && git -C $PROJECTS_ROOT/{} log | head -50"`
if [ -z "$quit" ]; then
zle reset-prompt
return 1
fi
BUFFER="pkill $quit"
elif [ $PROJECT = $LOCAL_OPTION ]; then # ローカル選択時
if ps | grep $PROJECT | grep -v grep > /dev/null; then
# アクティブプロジェクトの変更のみ
BUFFER="json-replace $CONFIG $ACTIVE_KEY '' && cd $LOCAL_ROOT"
else
# アクティブプロジェクトの変更 + ローカルの起動
BUFFER="json-replace $CONFIG $ACTIVE_KEY '' && cd $LOCAL_ROOT && \
node-red --title \"node-red $LOCAL_OPTION\" -p $PORT > /dev/null 2>&1 &"
fi
# MacOS and Google Chorome に対応、適宜修正
open -a "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"http://localhost:$PORT"
else # プロジェクト選択時
local pj_root=$PROJECTS_ROOT/$PROJECT
local pj_num=`echo $PROJECTS | grep -n $PROJECT | awk -F ':' '{print $1}'`
local port=`echo $(( $PORT + $pj_num - 2 ))`
if ps | grep $PROJECT | grep -v grep > /dev/null; then
# アクティブプロジェクトの変更のみ
BUFFER="json-replace $CONFIG $ACTIVE_KEY $PROJECT && cd $pj_root"
else
# アクティブプロジェクトの変更 + 該当プロジェクトの起動
BUFFER="json-replace $CONFIG $ACTIVE_KEY $PROJECT && cd $pj_root && \
node-red --title \"node-red $PROJECT\" -p $port > /dev/null 2>&1 &"
fi
# MacOS and Google Chorome に対応、適宜修正
open -a "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
"http://localhost:$port"
fi
zle accept-line
fi
}
# ショートカットの設定
zle -N nodered_active_pj
bindkey '^N' nodered_active_pj
以下の3つの変数は環境に合わせて、適宜修正してください。
変数 | 設定値 |
---|---|
NODE_RED | ユーザディレクトリ |
PORT | 任意 |
LOCAL_ROOT | 任意 |
また、以下の記述は Ctrl-N
を実行のトリガーとすることを意味します。
こちらも任意のショートカットに変更可能です。
bindkey '^N' nodered_active_pj
ファイルを作成・カスタマイズを終えたら、.zshrc
に以下を追記します。
source <</path/to/ファイル名>>
# 例:source nodered_pj_manager.sh
使い方
共通
Ctrl-N
を押下して、下記の出力を確認します。
なお、fzf ではインクリメンタル検索を行うことができます。
入力に応じて候補が自動的に絞り込まれるため、選択がしやすくなります。
例:a
を入力したため、候補がproject_a
と__LOCAL__
に絞られています。
<<プロジェクト名>>
、__LOCAL__
、QUIT
の中から選択します。
選択肢 | 結果 |
---|---|
<<プロジェクト名>> |
プロジェクトを起動してアクティブ化する |
__LOCAL__ |
ローカルで起動する |
QUIT |
起動中のプロセスからプロジェクトを選択・終了する |
特定プロジェクト/ローカル選択時
project_c
を選択したケースを例示します。
以下の出力を確認します。
自動でブラウザのタブが開かれます。
暫くしたらブラウザをリロードして、該当のプロジェクトが起動していること確認します。
選択するプロジェクトごとに自動で異なるポート番号が割り振られます。
そのため、複数のプロジェクトを同時に起動することが可能です。
なお、__LOCAL__
のみ1880
番ポートで固定になります。
ブラウザのタブを閉じてしまった場合、同様の手順でプロジェクトを選択すれば自動的にタブが開きます。
QUIT(終了)選択時
起動しているプロセスの一覧から、終了するプロジェクトを選択します。
以下の出力により、選択したプロジェクトが終了していることを確認します。
以上、簡単かつ爆速でプロジェクトを起動・切り替え・終了することができました。
参考
最後に
- Node-RED のプロジェクト機能や実装内容について、誤り・改善点があればご指摘お願いします
- pm2 などのプロセスマネージャーを使うともっと綺麗・簡単にできるのかもしれませんが、ルートディレクトリ問題も解消できるか検討し切れていません。
- 2021年も快適な Node-RED ライフを一緒に送りましょう!
明日は
明日の Node-RED Advent Calendar 2020 の担当は utaani さんの「Cloudflare for Teamsを使ったNode-REDへのリモートアクセス」です!