見出し画像

htmx でフルスタックの開発をする

(注:カバー画像は最近やっている刺繍・ダーニングで本文とは関係ありません)

htmx とは

htmx はフルスタック用のライブラリで、煩雑なデータの変換などをすっ飛ばしてバックエンドから DOM 操作をすることができます(厳密には違いますが使用感として)。普通のフルスタックではバックエンドから JSON などのデータをフロントエンドに投げて、それを div なりに成形してブラウザに表示します(例えば { message: "hello" } がサーバから送られてきて、それをフロントエンドで <div>hello</div> に変換する)。

普通のフルスタックの図

htmx はフロントエンドを飛び越えて(裏では htmx のコードがそれを代わりにやっているのですが)バックエンドから直接表示したい html タグを投げます(例えば POST リクエストへのレスポンスとして直接 <div>hello</div> を送る)。

htmx を使った場合の図

htmx はまだ比較的新しく日本語のリソースも少ないですが、海外のコミュニティでは実際にプロダクションで使われている例もあります。

これまで小さなプロジェクトで htmx を試していたので、一度自分の理解のためにもスターター用プロジェクトを作ってみました:https://glitch.com/edit/#!/htmx-starter

余談:ネットアーティストは htmx を使うべき?

メディアアートでは p5.js などキャンバスを使うタイプのサイトやフロントエンドがメインのレスポンシブなサイトが多いので htmx を使う機会はまだ少ない気がします。 htmx はチャットのようなインターフェイスやギャラリー的なレイアウトを作る場合など「テキスト重視」「コラボレーション系」「リスト表示」のアプリとは親和性が高いです。

プロジェクトの中身

図の通り htmx 自体はバックエンドの環境によらず動作しますが、今回は glitch でホスティングしていることもあり node.js と express を使うことにしました。また、フロントエンドのサーバーには Vite を使っています。

backend/server.js

node.js のバックエンドのコードで、express をインポートしてから api.js をロードします。

backend/api.js

こちらにバックエンドのエンドポイントを定義しています。express ルーターとして定義しているのである程度モジュラーに使えるようになっています(上記サーバーでルーターを /api 下にロードしているので、例えば /counter は最終的に /api/counter になります)。エンドポイントの仕様は次の節を参照してください。

backend/db.js

個人的に SQL が苦手なので acebase で JSON 形式でデータを格納しています。

frontend/index.html

ブラウザに読み込まれる html で、 CDN から htmx をロードしています。 rollup などのバンドルは今回は使っていませんが、サーバとして使っている Vite の標準機能に rollup が備わっているので簡単に追加できます。
また、 CSS には tailwind を用いているのでスタイルをクラスとしてサーバから送るデータに埋め込むことができます(もちろん Tachyons など他のライブラリや、 style アトリビュートに直接書き込んだり事前に定義したスタイルシートのクラスを使うのでもOKです)。

frontend/vite.config.js

今回は JavaScript のバンドルは使っていないのでシンプルですが、バックエンドへのプロキシのサーバ設定をしています。 /api/ 以下へのリクエストは全て別のポート(バックエンドの node.js)にフォワーディングされます。プロダクション環境では Vite を httpd や nginx に置き換えることになると思います。

API について

index.html で使っている htmx のタグを解説します。

<div hx-post="/api/counter" hx-trigger="load">

hx-trigger="load" は文字通りページのロード時にリクエストを実行します。このタグでは hx-post アトリビュートがあるので POST リクエストを投げますが、 hx-get にすれば GET リクエストもできます。デフォルトではサーバからのレスポンス(この場合は <div>You are the <span class="font-bold">${ i }</span>th visitor</div> )が本 div の innerHTML と置き換わります(なので、もともと書いてある loading の文字がサーバからのメッセージに置き換わる)。この動作は hx-swap を使うことで変更できます。

<button class="..." hx-post="/api/clicked" hx-target="closest div" hx-swap="outerHTML">

  • 上と同じく hx-post を使っていますが、今回は hx-trigger の指定されていない button なのでユーザがボタンを押した時にリクエストを投げます。

  • hx-target="closest div" はレスポンスをどの DOM と置き換えるか指定するアトリビュートです。デフォルトでは自分自身の DOM ですが、今回は closest div なので親の div になります。

  • hx-swap="outerHTML" は DOM のどこと置き換えるかの指定で、今回は outerHTML なので親 div そのものがレスポンスと置き換えられます(デフォルトは innerHTML)

<form class="..." hx-post="/api/echo" hx-swap="outerHTML" hx-on::after-request="this.reset()">

ユーザ入力が必要な場合は form タグを使います。通常は送信時にページ遷移しますが、 htmx ではページ遷移しないようになっています。フォームのテキストなどはバックエンドの express からは req.body オブジェクトを通してアクセスできます。

hx-on::after-request="..." ではリクエストを投げた時に実行する JavaScript を指定できます。今回はフォームの reset をしていますが、すぐにレスポンスと置き換わってしまうのであまり意味はありません。

<div class="..." hx-ext="sse" sse-connect="/api/incl-events" sse-swap="message" hx-on:htmx:after-settle="...">

こちらはもう少しアドバンスドな例で、サーバ送信イベント(Server-Sent Events: SSE)を使ったリアルタイムのタイムライン風のデモです。チャットやツイッターX的なインターフェイスが簡単に作れます。

  • hx-ext="sse" で拡張機能の SSE をロード。

  • sse-connect="/api/incl-events" で URL を指定。

  • sse-swap="message" でどのイベントを表示するか指定(サーバ送信イベントには event と data の二つの値があり、ここでは event をフィルタ的に使う)。

  • hx-on:htmx:after-settle="..." で指定された JavaScript コードがレンダー時に実行されます。ここでは scrollIntoView を使って一番下にあるエレメントにフォーカスします。

この記事が気に入ったらサポートをしてみませんか?