vault backup: 2026-04-28 08:53:28

This commit is contained in:
2026-04-28 08:53:28 +08:00
parent 2df3748fc9
commit 8b07798991
23 changed files with 3349 additions and 39 deletions
+239
View File
@@ -0,0 +1,239 @@
---
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 路由提供