Web開発
GoでHTMXをどのように使用するか
How I use HTMX with Go (alexedwards.net)
要約
この記事では、Go言語でWebアプリケーションにインタラクティブ性を追加するためにHTMXを使用する方法について解説しています。サーバーサイドHTMLレンダリングの利点を維持しつつ、JavaScriptの記述量を最小限に抑え、スムーズなアプリのような操作感を実現するパターンを紹介しています。テンプレートの構造化、部分/全ページHTMLレスポンスの送信、リダイレクトとエラー処理、HTMXの設定について、具体的なプロジェクト例を交えて説明します。
全文翻訳
GoのWebアプリケーションをどのように構造化すればよいか分かりませんか?私の新しい書籍では、Goでの実際のWebアプリケーションのゼロからの構築をガイドします。コードの構造化方法、依存関係の管理、動的なデータベース駆動型ページの作成、ユーザーの認証と認可を安全に行う方法などのトピックを扱っています。
年中セール:7月末まで30%オフ!ぜひご覧ください!
Webアプリケーションにインタラクティブな要素を追加したいとき、私はHTMXの使用を強く支持しています。操作にスムーズなアプリのような感触を与えるのが容易であること、記述しなければならないJavaScriptの量を最小限に抑えられること、そしてGoのhtml/templateパッケージによるサーバーサイドHTMLレンダリングの一貫性と安全性を維持できることが気に入っています。
この記事では、私が通常Goと組み合わせてHTMXをどのように使用するかを説明します。HTMXの仕組みについても少し触れますが、主な焦点はGo側のことになります。具体的には以下の点です。
HTMXと連携したHTMLテンプレートの構造化と、部分ページおよび全ページHTMLレスポンスの送信方法
HTMX使用時のリダイレクトとエラーの管理
私が使用する標準的なHTMX設定とその理由
これらの点を説明するために、最終的に以下のようなユーザーリストのフィルターを実装する小さなアプリケーションの構築を行います。
プロジェクトセットアップ
もしよろしければ、以下のコマンドを実行してプロジェクトのスケルトン構造を作成してください。
$ go mod init example.com/htmx
$ mkdir -p assets/static/css assets/static/img assets/static/js assets/html/partials assets/html/pages
$ touch assets/efs.go assets/html/base.tmpl assets/html/partials/images.tmpl assets/html/pages/home.tmpl cmd/web/main.go cmd/web/handlers.go cmd/web/html.go
これにより、以下のようなファイルツリーが得られるはずです。
.
├── assets
│ ├── efs.go
│ ├── html
│ │ ├── base.tmpl
│ │ ├── pages
│ │ │ └── home.tmpl
│ │ └── partials
│ │ └── images.tmpl
│ └── static
│ ├── css
│ ├── img
│ └── js
├── cmd
│ └── web
│ ├── handlers.go
│ ├── html.go
│ └── main.go
└── go.mod
HTMXのインストール
HTMXをインストールする方法はいくつかあります。CDNから読み込むことも、NPMを使ってインストールすることもできますが、私はほぼ常にコピーをダウンロードしてWebアプリケーションの静的ファイルとして提供しています。これはシンプルで、CDNを使用するデメリットを回避できます。
このデモプロジェクトのために、Bamboo(クラスレスCSSフレームワーク)と、github.com/egonelbre/gophers からのゴファーの画像もダウンロードします。以下のコマンドを実行して、これら3つすべてをassets/staticフォルダにダウンロードしてください。
$ wget -P assets/static/js https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js
$ wget -P assets/static/css https://cdn.jsdelivr.net/npm/[email protected]/dist/bamboo.min.css
$ wget -O assets/static/img/gopher.png https://raw.githubusercontent.com/egonelbre/gophers/refs/heads/master/sketch/misc/standing-left.png
これで、assets/staticの内容は以下のようになるはずです。
assets/static
├── css
│ └── bamboo.min.css
├── img
│ └── gopher.png
└── js
└── htmx.min.js
HTMLテンプレート
さて、プロジェクトのスケルトンと静的アセットが配置されたので、この記事の主要な部分に移り、HTMLテンプレートについて話しましょう。ほとんどすべてのプロジェクトで私の出発点は、以下のようなフォルダ構造を持つassets/htmlディレクトリです。
assets/html
├── base.tmpl
├── pages
│ └── home.tmpl
└── partials
└── images.tmpl
この構造の下では:
assets/html/base.tmpl ファイルには、すべてのWebページに共通のHTML「レイアウト」マークアップが含まれています。
assets/html/pages ディレクトリのファイルには、個々のWebページに対応するページ固有のコンテンツが含まれています。
assets/html/partials ディレクトリのファイルには、さまざまな場所で使用できる再利用可能なHTMLマークアップのチャンクが含まれています。
もしよろしければ、以下のマークアップをbase.tmplファイルに追加してください。
ファイル: assets/html/base.tmpl
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "page:title" .}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/css/bamboo.min.css">
<script defer src="/static/js/htmx.min.js"></script>
</head>
<body>
<h1><a href="/">Example website</a></h1>
<main>
{{template "page:content" .}}
</main>
</body>
</html>
{{end}}
この点についていくつか指摘があります。
<head>セクションでは、Bamboo CSSファイルとHTMX JavaScriptファイルをインポートしています。HTMXをインポートする際にはdefer属性を使用することに注意してください。これは、HTMXがWebページのHTMLを解析するのと並行してブラウザによって取得されますが、スクリプトはHTMLが完全に解析されDOMが構築されるまで実行されないことを意味します。deferがどのように機能し、なぜそれがここで正しい選択なのかを説明する優れたブログ記事があります。
HTMLテンプレートを作成する際には、すべてのテンプレートに明示的な名前を付けるようにしています。たとえ(この例のように)ファイルが1つのテンプレートしか含まず、それが厳密には必要ない場合でも、{{define}}...{{end}}アクションでマークアップを囲みます。あなたの好みは異なるかもしれませんが、私はGoコードから定義済みの名前でテンプレートを常に参照できる一貫性と明確さを好みます。定義済みの名前とファイル名の両方を混在させるよりもです。
テンプレート内では、{{template "page:title" .}} のようなアクションを使用して、適切なページ固有のコンテンツを正しい場所に挿入します。
話は変わりますが、次にホームページのページ固有のコンテンツを assets/html/pages/home.tmpl ファイルに追加しましょう。
ファイル: assets/html/pages/home.tmpl
{{define "page:title"}}Home{{end}}
{{define "page:content"}}
<button hx-get="/gopher" hx-swap="outerHTML"> Wanna see a cute gopher? </button>
{{end}}
このページには、2つのHTMX属性を持つボタンがあります:hx-get="/gopher" と hx-swap="outerHTML" です。これは、このボタンがクリックされると、HTMXがクリックをインターセプトし、アプリケーションに GET /gopher リクエストを送信し、アプリケーションが返したHTMLでDOM内のボタンを置き換えることを意味します。
最後に、ダウンロードしたゴファー画像を表示するためのHTMLを含むテンプレートを assets/html/partials/images.tmpl に追加しましょう。
ファイル: assets/html/partials/images.tmpl
{{define "partial:image:gopher"}}
<img alt="Gopher" src="/static/img/gopher.png" width="{{.}}">
{{end}}
このマークアップでは width="{{.}}" を使用していることに注意してください。これにより、動的な値を画像幅テンプレートに渡すことができます。
アセットの埋め込み
Go 1.16 でファイル埋め込みが導入されて以来、私は通常、実行時にディスクから読み取るのではなく、HTMLファイルと静的アセットをGoバイナリに埋め込んでいます。assets/html および assets/static ディレクトリの内容を埋め込み、それらを HTMLFiles および StaticFiles という2つのグローバル変数で利用できるように、assets/efs.go ファイルを更新しましょう。
ファイル: assets/efs.go
package assets
import (
"embed"
"io/fs"
)
//go:embed "html" "static"
var files embed.FS
var (
HTMLFiles = sub(files, "html")
StaticFiles = sub(files, "static")
)
func sub(f embed.FS, dir string) fs.FS {
sub, err := fs.Sub(f, dir)
if err != nil {
panic(err)
}
return sub
}
このコードでは、//go:embed "html" "static" ディレクティブが assets/html および assets/static ディレクトリの内容を files 変数(assets ディレクトリをルートとする embed.FS)に埋め込みます。その後、小さな sub() 関数を使用して、html および static ディレクトリをルートとする2つのサブファイルシステムを作成し、それらを HTMLFiles および StaticFiles 変数に割り当てました。
このように埋め込むことには2つの利点があります。
Goコードから使用する際に、静的ファイルとHTMLファイルの明確な分離を提供します。静的ファイルのみで動作することが意図されたコードは、HTMLファイルへの不要なアクセスを持たず、その逆も同様です。
HTMLFiles および StaticFiles ファイルシステムを使用するコードは、ファイルを開く際に html/ または static/ パスプレフィックスを含める必要がありません。
HTMLテンプレートのレンダリング
HTTPレスポンスでHTMLテンプレートをレンダリングするために、htmlRenderer という型を作成するのが良いパターンだと分かりました。この型は、a) 起動時に共有テンプレートのセットを解析し、b) レンダリング機能を提供します。