我如何在 Go 项目中使用 HTMX
还在为如何组织 Go Web 项目的代码结构而烦恼?
我的新书将带你从头到尾完成一个真实的 Web 应用开发——涵盖代码结构设计、依赖管理、动态数据库页面构建,以及如何安全地进行用户认证和授权。
年中大促:7月底前享 7 折优惠!
去看看!
当我想给 Web 应用添加一些交互能力时,我非常喜欢用 HTMX。它让交互体验变得流畅、有 App 般的感觉,同时大幅减少了我需要写的 JavaScript 代码量。更重要的是,它让我能继续使用 Go 的 html/template 包,享受服务端渲染带来的一致性和安全性。
这篇文章里,我来聊聊我在 Go 项目中通常是怎么用 HTMX 的。虽然会简单介绍 HTMX 的工作原理,但重点还是会放在 Go 这边。具体来说:
- 我如何组织 HTML 模板,以及如何向 HTMX 返回局部和完整页面 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 cmd/web
$ 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 引入,也可以下载到本地。我个人比较喜欢把 HTMX 下载到项目的 static 目录里,然后通过 Go 的嵌入式文件系统(embedded filesystem)来提供服务。这样做的好处是部署起来更简单,不依赖外部 CDN,而且离线环境也能正常工作。
下面我们把 HTMX 下载到 assets/static/js 目录:
$ curl -o assets/static/js/htmx.min.js https://unpkg.com/htmx.org@1.9.10/dist/htmx.min.js
如果你想在开发时使用 HTMX 的事件调试功能,还可以下载那个调试版本的脚本:
$ curl -o assets/static/js/htmx.ext.debug.js https://unpkg.com/htmx.org@1.9.10/dist/ext/debug.js
基础设置
接下来我们设置 HTML 模板的基础布局。我会创建一个 base.tmpl 文件,包含标准的 HTML 头部和必要的 HTMX 加载脚本:
{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{template "title" .}}</title>
<link rel="stylesheet" href="/static/css/main.css">
<script src="/static/js/htmx.min.js"></script>
</head>
<body>
{{end}}
然后创建 home.tmpl 页面作为主入口:
{{template "base" .}}
{{define "title"}}Home{{end}}
<main>
<h1>User Management</h1>
<div id="user-list" hx-get="/users" hx-trigger="load">
Loading users...
</div>
</main>
{{end}}
HTMX 的核心概念
HTMX 的工作方式非常优雅。你只需要在 HTML 元素上添加一些特殊的属性,HTMX 就会自动处理 AJAX 请求、更新页面内容,而无需写任何 JavaScript 代码。
几个最常用的属性包括:
hx-get、hx-post等:定义 HTTP 方法和目标 URLhx-target:指定哪个元素会被更新hx-trigger:定义触发条件(点击、悬停、加载等)hx-swap:指定如何插入新内容(innerHTML、outerHTML 等)
这个设计让我可以完全在服务端处理复杂的业务逻辑,然后用 HTML 片段的形式返回给前端,HTMX 负责把内容渲染到指定位置。非常干净,也非常好维护。
返回局部 HTML 响应
对于 HTMX 请求,最常见的场景是返回一个局部的 HTML 片段(partial)。比如我们想要实现一个用户列表的筛选功能:
// cmd/web/handlers.go
package main
import (n "net/http"
"github.com/alexedwards/flow"
)
func home(w http.ResponseWriter, r *http.Request) {
ts := templateFiles("html/pages/home.tmpl")
err := ts.ExecuteTemplate(w, "home", nil)
if err != nil {
serverError(w, err)
}
}
func getUsers(w http.ResponseWriter, r *http.Request) {
// 从数据库获取用户列表
users, err := db.GetUsers()
if err != nil {
serverError(w, err)
return
}
// 返回局部 HTML 片段
ts := templateFiles("html/partials/user-list.tmpl")
err = ts.ExecuteTemplate(w, "user-list", users)
if err != nil {
serverError(w, err)
}
}
user-list.tmpl 模板只需要包含用户列表的 HTML,不需要完整的页面结构:
{{define "user-list"}}
{{range .}}
<div class="user-item">
<span>{{.Name}}</span>
<span>{{.Email}}</span>
<span>{{.Role}}</span>
</div>
{{end}}
{{end}}
处理筛选请求
现在我们来实现筛选功能。用户可以在下拉框中选择角色,HTMX 会发送一个带参数的请求到服务端,服务端返回过滤后的用户列表:
func filterUsers(w http.ResponseWriter, r *http.Request) {
role := r.URL.Query().Get("role")
users, err := db.GetUsersByRole(role)
if err != nil {
serverError(w, err)
return
}
ts := templateFiles("html/partials/user-list.tmpl")
err = ts.ExecuteTemplate(w, "user-list", users)
if err != nil {
serverError(w, err)
}
}
模板中添加筛选下拉框:
{{template "base" .}}
{{define "title"}}User Management{{end}}
<main>
<h1>User Management</h1>
<select id="role-filter"
hx-get="/users/filter"
hx-trigger="change"
hx-target="#user-list"
hx-include="[name='role']"
name="role">
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
</select>
<div id="user-list" hx-get="/users" hx-trigger="load">
Loading users...
</div>
</main>
{{end}}
处理错误和重定向
使用 HTMX 时,错误处理和普通请求略有不同。默认情况下,如果服务端返回错误,HTMX 会把错误信息直接显示在页面上,有时候用户体验不太好。
我通常会在 Go 服务端设置一个自定义的错误处理逻辑,当请求来自 HTMX 时,返回格式化的错误信息:
func serverError(w http.ResponseWriter, err error) {
if isHTMX(r) {
// HTMX 请求返回局部错误信息
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `<div class="error">%s</div>`, err.Error())
return
}
// 普通请求显示错误页面
http.Error(w, err.Error(), http.StatusInternalServerError)
}
func isHTMX(r *http.Request) bool {
return r.Header.Get("Hx-Request") == "true"
}
对于重定向,HTMX 支持通过响应头 HX-Redirect 来实现客户端重定向:
func logout(w http.ResponseWriter, r *http.Request) {
session.Destroy(r)
if isHTMX(r) {
w.Header().Set("HX-Redirect", "/login")
w.WriteHeader(http.StatusOK)
return
}
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
我的 HTMX 配置
最后分享几个我常用的 HTMX 全局配置:
document.body.addEventListener('htmx:configRequest', function(event) {
// 为所有请求添加 CSRF token
event.detail.headers['X-CSRF-Token'] = '{{.CSRFToken}}';
});
document.body.addEventListener('htmx:afterRequest', function(event) {
// 请求完成后清除 loading 状态
event.target.classList.remove('htmx-loading');
});
// 全局错误处理
document.body.addEventListener('htmx:responseError', function(event) {
alert('请求失败,请稍后重试');
});
总结
HTMX + Go 是一个非常舒服的组合。你可以在服务端用 Go 处理所有复杂的业务逻辑,渲染 HTML 模板,然后通过 HTMX 实现流畅的前端交互。代码量少、可维护性强,而且整个应用的结构非常清晰。
如果你还没试过 HTMX,建议先从一个小功能开始体验,比如表单验证、列表筛选或者评论系统。相信你会爱上这种开发方式的。
