LoginSignup
3
2

htmx + Node-RED

Last updated at Posted at 2024-02-29

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 ノードに上記サンプルをそのまま書きます。

スクリーンショット 2024-02-29 22.09.09.png

http in ノードと http response ノードで挟んで GET アクセスできるようにします。

スクリーンショット 2024-02-29 22.13.25.png

http://localhost:1880/htmx にアクセスしてみると上記サンプルと同じ挙動になります。

スクリーンショット 2024-02-29 22.15.01.png

次に JSONPlaceholder をローカルの API に置き換える

データが必要ですが、とりあえず flow コンテキストを使います。先ほど作ったページにアクセスしたら flow.members を初期化する change ノードを追加します。

スクリーンショット 2024-02-29 22.21.56.png

change ノードの設定はこんな感じです。

スクリーンショット 2024-02-29 22.24.57.png

データの中身はこんな感じ。

スクリーンショット 2024-02-29 22.26.12.png

このデータを使って /members というメンバーリストを返すエンドポイントと、 /member?id=xxx でリクエストのあった ID のメンバーだけを返す2つのエンドポイントを作ります。
(API 的に美しくないですがとりあえず)

エンドポイント /members の作成

フロー全体は以下のような感じです。

スクリーンショット 2024-02-29 22.33.29.png

template ノードの設定はこんな感じです。

スクリーンショット 2024-02-29 22.34.27.png

あとは最初に作った /htmx の template ノードを以下のように書き換えます。

スクリーンショット 2024-02-29 22.37.34.png

スクリーンショット 2024-02-29 22.38.08.png

http://localhost:1880/htmx にアクセスしてボタンを押したら以下のようになります。

スクリーンショット 2024-02-29 22.40.04.png

エンドポイント /member の作成

フロー全体は以下のような感じです。

スクリーンショット 2024-02-29 22.40.25.png

change ノードの設定はこんな感じです。

スクリーンショット 2024-02-29 22.42.02.png

JSONata 式が複雑ですが、とりあえず絞り込むためです...(もう function ノードでいいじゃんて言わないで)

スクリーンショット 2024-02-29 22.43.33.png

template ノードの設定はこんな感じです。

スクリーンショット 2024-02-29 22.45.59.png

あとは最初に作った /htmx の template ノードを以下のように書き換えます。

スクリーンショット 2024-02-29 22.49.34.png

http://localhost:1880/htmx にアクセスしてボタンを押したら以下のようになります。

スクリーンショット 2024-02-29 22.49.55.png

フロー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 と組み合わせることでユーザー体験もそんなに落とさずシンプルなフロントエンド開発ができるんじゃないか!ということで試してみたという記事でした〜(どこまでイケるか、もうちょっとキャッチアップ続けます)

なんていうか 温故知新 ですな(使い方合ってる?)

3
2
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
3
2