我在 Go 后端里试了 htmx,才发现前后端分离根本不是唯一解
我在 Go 后端里试了 htmx,才发现前后端分离根本不是唯一解
导语
过去几年,前端圈一直在讲「前后端分离」「SPA」「 hydration 」「 islands 架构」。但最近越来越多开发者开始问:做一个普通的增删改查页面,真的需要这套完整的 React/Vue 工程化吗?这篇文章把我在 Go 后端里接入 htmx 的实践过程完整记录下来,看完你会明白:有些场景,一行 JS 都不用写也能做出现代 Web 应用。
正文
一、问题:为什么我开始怀疑「全前端」的必要性
我在 2025 年接了一个内部工具项目,需求很简单:一个表格、一个表单、一个搜索栏。按以前的习惯,我直接搭了一个 Next.js 项目,配上 Tailwind、Zustand、TanStack Query,第一天就在处理依赖冲突和构建配置。
后来我统计了一下:整个项目里,真正复杂的交互不超过 3 处,其余 90% 都是标准的 CRUD。而为了这 10% 的复杂度,我引入了整整 200 多个 npm 包,首屏加载 180KB gzip,本地冷启动要 12 秒。
这时候我开始认真看 htmx。
二、方案:htmx + Go 的组合到底怎么玩
htmx 的核心思路非常简单:用 HTML 属性描述交互,服务器返回 HTML 片段,前端负责局部更新。不需要编译、不需要构建、不需要 JS 框架。
我用 Gin 框架搭了一个最小例子,把 htmx 接进去:
package main
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
type Todo struct {
ID int `json:"id"`
Text string `json:"text"`
Done bool `json:"done"`
}
var todos = []Todo{
{ID: 1, Text: "试试 htmx", Done: false},
{ID: 2, Text: "写一篇实战文章", Done: true},
}
func main() {
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.GET("/", index)
r.POST("/todos", addTodo)
r.POST("/todos/:id/toggle", toggleTodo)
r.DELETE("/todos/:id", deleteTodo)
r.Run(":8080")
}
func index(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"Todos": todos,
})
}
func addTodo(c *gin.Context) {
text := c.PostForm("text")
if text != "" {
todos = append(todos, Todo{
ID: len(todos) + 1,
Text: text,
Done: false,
})
}
c.HTML(http.StatusOK, "partials/todo-item.html", gin.H{
"Todo": todos[len(todos)-1],
})
}
前端的 index.html 长这样:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Go + htmx Todo</title>
<script src="https://unpkg.com/htmx.org@2.0.0"></script>
<style>
.done { text-decoration: line-through; opacity: 0.6; }
</style>
</head>
<body>
<h1>待办清单</h1>
<form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend">
<input type="text" name="text" placeholder="加一个新待办" required>
<button type="submit">添加</button>
</form>
<ul id="todo-list">
{{range .Todos}}
{{template "todo-item" .}}
{{end}}
</ul>
</body>
</html>
而单个待办的局部模板 todo-item.html:
<li id="todo-{{.ID}}" class="{{if .Done}}done{{end}}">
<input type="checkbox"
{{if .Done}}checked{{end}}
hx-post="/todos/{{.ID}}/toggle"
hx-target="#todo-{{.ID}}"
hx-swap="outerHTML">
{{.Text}}
<button hx-delete="/todos/{{.ID}}"
hx-target="#todo-{{.ID}}"
hx-swap="outerHTML">删除</button>
</li>
看到这里你可能会说:「这不就是服务器端渲染 + AJAX 吗?」没错,但 htmx 把这两件事做得足够优雅。
三、体验对比:5 个维度说说真实感受
我把这个 demo 和一个功能相同的 React + TanStack Query 版本做了对比,结果很值得注意:
1. 首次加载时间
- React 版本:首屏 JS 180KB,LCP ~1.2s
- htmx 版本:首屏只有 HTML + 14KB 的 htmx.min.js,LCP ~0.3s
2. 开发体验
- React 需要写组件、管理 state、处理 loading/error 状态
- htmx 只需要写 HTML 模板,后端处理所有逻辑
3. 调试难度
- React 的 bug 经常出现在「数据什么时候同步」这个环节
- htmx 的 bug 基本都在后端,调试链路更短
4. SEO 与可访问性
- htmx 输出的是标准 HTML,对爬虫和屏幕阅读器完全友好
- React 需要额外处理 SSR 或 SSG
5. 复杂交互上限
- 这是 htmx 的软肋:涉及大量客户端状态时,代码会变得很难维护
- 我个人的经验是:如果交互不超过 10 处,htmx 完胜;超过这个数,考虑引入 JS 框架
四、边界:哪些场景不适合用 htmx
htmx 不是银弹。以下几个场景我建议还是用现代前端框架:
- 需要频繁客户端状态管理的富交互应用(如 Figma、Notion)
- 需要离线能力或复杂 PWA 特性
- 团队成员对 HTML 属性模式不熟悉
- 需要精细控制动画和过渡效果
但如果你在做的是一个典型的 CRUD 后台、一个 CMS、一个表单密集的工具,htmx 完全能打,而且开发速度往往更快。
五、结论与下一步
htmx 的最大价值不是「消灭 JavaScript」,而是帮你重新思考前端复杂度。很多时候我们引入了复杂工具链,不是因为需求真的需要,而是因为「别人都这么做」。
我的建议是:下次遇到一个新项目,先问自己三个问题——
- 这个页面真的需要 SPA 吗?
- 大部分交互能不能用简单的 HTML 表单 + 局部刷新实现?
- 我引入的前端框架,是在解决问题还是在制造问题?
如果三个问题中有两个答案是「不需要/否」,那 htmx 值得你花一个下午试试。
如果你也试过 htmx,欢迎在评论区聊聊你的真实体验。
评论区
登录后可评论。












