我在 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,欢迎在评论区聊聊你的真实体验。

评论区

0 条评论

登录后可评论。

查看完整榜单
查看完整榜单
查看完整榜单