htmx って何?
この記事見ると海外で流行ってるらしいです。
簡単に言うと javascript 書かないで HTML だけで Ajax (懐かしい)できるという話。以下のような HTML だけでボタンが /clicked
のコンテンツに置き換わる。
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<!-- have a button POST a click via AJAX -->
<button hx-post="/clicked" hx-swap="outerHTML">
Click Me
</button>
上の記事では JSONPlaceholder というダミーURLを使った以下のようなサンプルがあって、これを動かすとどういうことかよくわかります。
See the Pen Untitled by Tomoki Ota (@odlqzyes-the-sans) on CodePen.
これはNode-RED向きなんじゃないか?
以前、Node-RED でフロントエンドどうするのか?ってイベントやった時に node-red-contrib-uibuilder を使った開発を紹介しました。
(SlideShare 久々に見たけどひどい...)
この時、 React や Vue を知ってる上で多少のコーディングが前提だったんでローコードとしては不評でした(劇的にコード量減らせるんだけど)
そこに、この htmx、これってもう前提が HTML しかなくない?ということで率直に Node-RED に向いてると思いました。
Node-RED で試してみる
まずは上記サンプルの移植
template ノードに上記サンプルをそのまま書きます。
http in ノードと http response ノードで挟んで GET アクセスできるようにします。
http://localhost:1880/htmx
にアクセスしてみると上記サンプルと同じ挙動になります。
次に JSONPlaceholder をローカルの API に置き換える
データが必要ですが、とりあえず flow コンテキストを使います。先ほど作ったページにアクセスしたら flow.members
を初期化する change ノードを追加します。
change ノードの設定はこんな感じです。
データの中身はこんな感じ。
このデータを使って /members
というメンバーリストを返すエンドポイントと、 /member?id=xxx
でリクエストのあった ID のメンバーだけを返す2つのエンドポイントを作ります。
(API 的に美しくないですがとりあえず)
エンドポイント /members
の作成
フロー全体は以下のような感じです。
template ノードの設定はこんな感じです。
あとは最初に作った /htmx
の template ノードを以下のように書き換えます。
http://localhost:1880/htmx
にアクセスしてボタンを押したら以下のようになります。
エンドポイント /member
の作成
フロー全体は以下のような感じです。
change ノードの設定はこんな感じです。
JSONata 式が複雑ですが、とりあえず絞り込むためです...(もう function ノードでいいじゃんて言わないで)
template ノードの設定はこんな感じです。
あとは最初に作った /htmx
の template ノードを以下のように書き換えます。
http://localhost:1880/htmx
にアクセスしてボタンを押したら以下のようになります。
フローJSONは以下です。
[
{
"id": "334995aafa9a54b3",
"type": "tab",
"label": "フロー 2",
"disabled": false,
"info": "",
"env": []
},
{
"id": "ace3b2921ec2a993",
"type": "http in",
"z": "334995aafa9a54b3",
"name": "",
"url": "/htmx",
"method": "get",
"upload": false,
"swaggerDoc": "",
"x": 120,
"y": 100,
"wires": [
[
"d185b1236d3a130b"
]
]
},
{
"id": "35a6b86280d8a98a",
"type": "template",
"z": "334995aafa9a54b3",
"name": "",
"field": "payload",
"fieldType": "msg",
"format": "html",
"syntax": "mustache",
"template": "<script src=\"https://unpkg.com/htmx.org@1.9.10\"></script>\n\n<h2>メンバーリスト取得</h2>\n<button hx-get=\"http://localhost:1880/members\">Click</button>\n\n<h2>メンバーリスト絞り込み</h2>\n<button hx-get=\"http://localhost:1880/member?id=001\">Click</button>",
"output": "str",
"x": 500,
"y": 100,
"wires": [
[
"38e3a17b24c944da"
]
]
},
{
"id": "38e3a17b24c944da",
"type": "http response",
"z": "334995aafa9a54b3",
"name": "",
"statusCode": "",
"headers": {},
"x": 650,
"y": 100,
"wires": []
},
{
"id": "d185b1236d3a130b",
"type": "change",
"z": "334995aafa9a54b3",
"name": "",
"rules": [
{
"t": "set",
"p": "members",
"pt": "flow",
"to": "[{\"id\":\"001\",\"name\":\"kojo\"},{\"id\":\"002\",\"name\":\"yokoi\"},{\"id\":\"003\",\"name\":\"taiji\"},{\"id\":\"004\",\"name\":\"seigo\"}]",
"tot": "json"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 310,
"y": 100,
"wires": [
[
"35a6b86280d8a98a"
]
]
},
{
"id": "0ecd4f0590fdcc00",
"type": "http in",
"z": "334995aafa9a54b3",
"name": "",
"url": "/member",
"method": "get",
"upload": false,
"swaggerDoc": "",
"x": 130,
"y": 220,
"wires": [
[
"0b0049b2eeae6f11"
]
]
},
{
"id": "0b0049b2eeae6f11",
"type": "change",
"z": "334995aafa9a54b3",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "$filter($flowContext(\"members\"), function($v, $i, $a) {\t $v.id = payload.id\t})\t",
"tot": "jsonata"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 320,
"y": 220,
"wires": [
[
"6105b6b0d00ba411"
]
]
},
{
"id": "df7e7f134ed6fa02",
"type": "http response",
"z": "334995aafa9a54b3",
"name": "",
"statusCode": "",
"headers": {},
"x": 650,
"y": 220,
"wires": []
},
{
"id": "6105b6b0d00ba411",
"type": "template",
"z": "334995aafa9a54b3",
"name": "",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "<ul>\n {{#payload}}\n <li>{{name}}</li>\n {{/payload}}\n</ul>",
"output": "str",
"x": 500,
"y": 220,
"wires": [
[
"df7e7f134ed6fa02"
]
]
},
{
"id": "fc3a2399a25af660",
"type": "http in",
"z": "334995aafa9a54b3",
"name": "",
"url": "/members",
"method": "get",
"upload": false,
"swaggerDoc": "",
"x": 140,
"y": 160,
"wires": [
[
"7a77d5e11c31c411"
]
]
},
{
"id": "03a80983146f5cb3",
"type": "http response",
"z": "334995aafa9a54b3",
"name": "",
"statusCode": "",
"headers": {},
"x": 510,
"y": 160,
"wires": []
},
{
"id": "7a77d5e11c31c411",
"type": "template",
"z": "334995aafa9a54b3",
"name": "",
"field": "payload",
"fieldType": "msg",
"format": "handlebars",
"syntax": "mustache",
"template": "<ul>\n {{#flow.members}}\n <li>{{name}}</li>\n {{/flow.members}}\n</ul>",
"output": "str",
"x": 340,
"y": 160,
"wires": [
[
"03a80983146f5cb3"
]
]
}
]
より実践的なお試し記事はこちら!
なにが Node-RED 向きなのか?
触ってみた感じ htmx はサーバサイドレンダリングへの回帰だと感じました。Next.js とかの流れで SSR が復活してきてますが、 htmx はページの一部をサーバサイドレンダリングしたコンテンツに置き換えることでフロントエンド開発の複雑な部分を解消しつつ、ユーザー体験もそんなに落とさないという絶妙なラインを責めてるように思いました。
これまでも Node-RED で http 系ノードと template ノードを使って Web ページをレンダリングできましたが、すべてサーバサイドレンダリングの画面遷移になるので1990年代のユーザー体験を強いてました。
そこで、この絶妙な合いの子である htmx と組み合わせることでユーザー体験もそんなに落とさずシンプルなフロントエンド開発ができるんじゃないか!ということで試してみたという記事でした〜(どこまでイケるか、もうちょっとキャッチアップ続けます)
なんていうか 温故知新 ですな(使い方合ってる?)