Go语言:使用Templ构建类型安全的HTML模板

Go 标准库的 html/template 用久了就知道它的痛点:没有类型检查、重构困难、IDE 支持差。Templ 是一个新思路——在编译期把 .templ 文件生成为 Go 代码,提供完整的类型安全和 IDE 补全。最近在项目中试用了一段时间,体验确实好很多。

Templ vs html/template

对比项 html/template Templ
类型安全 无,运行时才报错 编译期类型检查
IDE 支持 基本没有 LSP 支持,补全/跳转/重构
组件化 需要手动管理 partial 原生组件系统
性能 运行时解析模板 编译为 Go 代码,零运行时开销
学习曲线 Go 模板语法 类 JSX 语法
与 Go 集成 传 interface{} 直接传 Go 类型

安装与基本使用

go install github.com/a-h/templ/cmd/templ@latest

创建第一个 .templ 文件 hello.templ

package pages

templ Hello(name string) {
    <div class="greeting">
        <h1>Hello, { name }!</h1>
        <p>Welcome to Templ.</p>
    </div>
}

生成 Go 代码:

templ generate

这会生成 hello_templ.go,包含一个 func Hello(name string) templ.Component 函数。在 handler 中使用:

package main

import (
    "net/http"
    "myapp/pages"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        pages.Hello("World").Render(r.Context(), w)
    })
    http.ListenAndServe(":8080", nil)
}

组件化

Templ 的组件就是函数,可以自然地组合:

package components

// 布局组件,children 是插槽
templ Layout(title string) {
    <!DOCTYPE html>
    <html lang="zh-CN">
        <head>
            <meta charset="UTF-8"/>
            <title>{ title }</title>
            <link rel="stylesheet" href="/static/style.css"/>
        </head>
        <body>
            <nav>
                @NavBar()
            </nav>
            <main>
                { children... }
            </main>
            <footer>
                @Footer()
            </footer>
        </body>
    </html>
}

templ NavBar() {
    <div class="nav">
        <a href="/">Home</a>
        <a href="/about">About</a>
    </div>
}

templ Footer() {
    <div class="footer">
        <p>&copy; 2024</p>
    </div>
}

页面组件使用布局:

package pages

import "myapp/components"

templ HomePage(posts []Post) {
    @components.Layout("首页") {
        <h1>最近文章</h1>
        for _, post := range posts {
            @PostCard(post)
        }
    }
}

templ PostCard(post Post) {
    <article class="card">
        <h2>{ post.Title }</h2>
        <time>{ post.CreatedAt.Format("2006-01-02") }</time>
        <p>{ post.Summary }</p>
    </article>
}

注意 { children... } 语法——这是 Templ 的插槽机制,等价于 React 的 props.children

条件与循环

templ UserProfile(user User) {
    <div class="profile">
        <h2>{ user.Name }</h2>

        // 条件渲染
        if user.IsAdmin {
            <span class="badge">管理员</span>
        } else {
            <span class="badge">普通用户</span>
        }

        // 循环
        if len(user.Tags) > 0 {
            <ul>
                for _, tag := range user.Tags {
                    <li>{ tag }</li>
                }
            </ul>
        }
    </div>
}

语法和 Go 完全一致,没有额外的模板语法需要学习。

与 htmx 配合

Templ + htmx 是当前 Go Web 开发一个非常香的组合——服务端渲染 + 局部更新,不需要写 JavaScript:

templ TodoList(todos []Todo) {
    <div id="todo-list">
        for _, todo := range todos {
            @TodoItem(todo)
        }
    </div>
    <form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend">
        <input type="text" name="title" placeholder="新任务..." />
        <button type="submit">添加</button>
    </form>
}

templ TodoItem(todo Todo) {
    <div class="todo-item" id={ fmt.Sprintf("todo-%d", todo.ID) }>
        <input type="checkbox"
            if todo.Done {
                checked
            }
            hx-patch={ fmt.Sprintf("/todos/%d/toggle", todo.ID) }
            hx-target={ fmt.Sprintf("#todo-%d", todo.ID) }
            hx-swap="outerHTML"
        />
        <span>{ todo.Title }</span>
    </div>
}

htmx 请求后端返回 HTML 片段,Templ 负责渲染片段——职责分明,代码量小。

热重载

开发时用 templ generate --watch 监听文件变化:

# 终端 1:监听 .templ 文件变化,自动生成 Go 代码
templ generate --watch

# 终端 2:用 air 做 Go 代码的热重载
air

推荐配合 air 使用。.air.toml 中把 _templ.go 加入监听范围:

[build]
  cmd = "go build -o ./tmp/main ."
  include_ext = ["go", "templ"]
  exclude_regex = ["_test.go"]

小结

Templ 在 Go 模板方案中是一个很实际的进步。类型安全和 IDE 支持解决了 html/template 的核心痛点,组件化和与 htmx 的配合让它在中小型 Web 项目中非常好用。缺点是多了一步代码生成,CI 配置要注意把 templ generate 加到构建流程里。