This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/BACKEND/GIN/10-template-rendering.md
T

240 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [后端, Go, Gin, 模板, HTML]
create time: 2026-04-28 00:00
---
# HTML 模板渲染
## 概述
Gin 内建了对 Go 标准库 `html/template` 的封装,支持简单模板加载、多模板引擎配置、以及通过 `embed.FS` 将模板打包进单一二进制。虽然现代前后端分离架构中较少直接使用服务端渲染,但在管理后台、邮件模板等场景中仍然实用。
思考题:`c.HTML` 和 `http.ServeFile` 直接返回 `.html` 文件有什么区别?
## 正文
### 1. 基本模板渲染
```go
func main() {
r := gin.Default()
// 加载 templates/ 目录下所有 .html 文件
r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) {
// 渲染 templates/index.html,传入模板数据
c.HTML(http.StatusOK, "index", gin.H{
"title": "Home Page",
"user": "wonder",
})
})
r.GET("/news", func(c *gin.Context) {
c.HTML(http.StatusOK, "news.tmpl", gin.H{
"title": "News",
"items": []string{"item1", "item2"},
})
})
r.Run(":8080")
}
```
模板文件 `templates/index.html`:
```html
<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<h1>Welcome, {{.User}}</h1>
<!-- 遍历切片 -->
{{range .Items}}
<li>{{.}}</li>
{{end}}
</body>
</html>
```
> **关键点:** 第二个参数是模板文件名(不带路径),Gin 根据 `LoadHTMLGlob` 或 `LoadHTMLFiles` 配置的映射来查找。
### 2. `LoadHTMLGlob` vs `LoadHTMLFiles`
| 方法 | 用途 | 示例 |
|------|------|------|
| `LoadHTMLGlob(pattern)` | 按 glob 模式加载一批模板 | `"templates/**/*.html"` |
| `LoadHTMLFiles(paths...)` | 指定具体的文件列表 | `"templates/base.html", "templates/index.html"` |
```go
// Glob — 适合模板较多、结构简单的场景
r.LoadHTMLGlob("templates/**/*") // 包括子目录
// Files — 适合明确知道有哪些模板的场景
r.LoadHTMLFiles(
"templates/base.html",
"templates/index.html",
"templates/error.html",
)
```
> **陷阱:** `LoadHTMLGlob("templates/**/*")` 会用每个文件的**基名**作为模板名。如果 `templates/sub/page.html` 也被加载,模板名就是 `sub/page.html`——渲染时需写 `c.HTML(200, "sub/page.html", data)`。
### 3. 多模板引擎(Template.FuncMap)
可以注册自定义模板函数,类似 Python Jinja2 的 filter:
```go
func main() {
r := gin.Default()
r.SetFuncMap(template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
"truncate": func(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
},
"upper": strings.ToUpper,
})
r.LoadHTMLGlob("templates/*")
r.GET("/article", func(c *gin.Context) {
c.HTML(200, "article", gin.H{
"title": "Gin Templating Guide",
"body": "这是一篇很长的文章...",
"date": time.Now(),
})
})
r.Run(":8080")
}
```
模板中使用:
```html
<h1>{{.Title | upper}}</h1>
<p>{{.Body | truncate 50}}</p>
<small>发布于 {{.Date | formatDate}}</small>
```
### 4. 模板继承(Base Template)
Go 原生模板不支持继承,但可以通过 `define` + `template` 模拟:
```html
<!-- templates/base.html -->
<!DOCTYPE html>
<html>
<head>
<title>{{block "title" .}}Default{{end}}</title>
</head>
<body>
<nav>...</nav>
{{block "content" .}}{{end}}
<footer>...</footer>
</body>
</html>
<!-- templates/index.html — 引用 base -->
{{define "title"}}Home{{end}}
{{define "content"}}
<h1>Welcome</h1>
<p>{{.Message}}</p>
{{end}}
```
渲染时传入组合后的模板:
```go
r.LoadHTMLFiles(
"templates/base.html",
"templates/index.html",
)
// 此时 index.html 中的 {{template "base"}} 才能找到定义
```
### 5. 将模板打包进单一二进制(Go 1.16+)
使用 `embed.FS` 把模板文件嵌入 Go 编译产物,适合 Docker 单镜像部署:
```go
import _ "embed"
import "html/template"
//go:embed templates/*.html
var templateFS embed.FS
func main() {
r := gin.Default()
// 从 embedded FS 加载模板
r.LoadHTMLTemplates(template.New("").ParseFS(templateFS, "templates/*.html"))
r.Run(":8080")
}
```
```go
// Go 1.23+ 写法
r.LoadHTMLTemplates(template.Must(template.New("").ParseFS(templateFS, "templates/*.html")))
```
**项目目录结构:**
```
cmd/
├── server/main.go ← embed 入口
templates/ ← 模板文件,不会被 gitignore
├── base.html
├── index.html
└── error.html
internal/
└── handlers/
```
> **优势:** 部署时只需一个二进制文件,不再需要挂载 Volume 或复制模板文件到容器中。
### 6. 模板安全注意事项
```mermaid
flowchart LR
A["用户输入"] --> B["存入 gin.H 数据"]
B --> C["传入 c.HTML 渲染"]
C --> D{是否 HTML 转义?}
D -->|"是 ✅"| E["自动转义,安全"]
D -->|"否 ❌"| F["XSS 漏洞"]
style E fill:#e8f5e9
style F fill:#ffebee
```
Go 的 `html/template` 包**自动对上下文相关内容进行转义**:
- 在 HTML body 中 → 转义 `<>&"'`
- 在 attribute 中 → 转义引号和 `<>&`
- 在 JS 上下文中 → 转义 `'` 和 `<\/`
**唯一例外:** 使用 `template.HTML` / `template.JS` 类型包装的内容不会转义——这意味着你主动告诉模板"这段内容是安全的"。滥用会导致 XSS:
```go
// ❌ 危险:用户输入未过滤就标为 safe
c.HTML(200, "page", gin.H{
"content": template.HTML(userInput), // 可能被注入 <script>
})
// ✅ 安全:让模板自己决定如何转义
c.HTML(200, "page", gin.H{
"content": userInput, // html/template 自动转义
})
```
## 关联笔记
- [[GIN/9-response-rendering]] — 除了 HTML,Gin 还支持 JSON/XML 等多种渲染
- [[GIN/11-static-files]] — 静态资源(CSS/JS/图片)通过 Static 路由提供