← 文章 / 科技资讯
alexedwards 10天前 · 2026-07-15 17:10:50 · 1 阅读

我如何在 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-gethx-post 等:定义 HTTP 方法和目标 URL
  • hx-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,建议先从一个小功能开始体验,比如表单验证、列表筛选或者评论系统。相信你会爱上这种开发方式的。

原始来源: alexedwards

评论 (0)