vault backup: 2026-04-28 08:53:28
This commit is contained in:
@@ -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 路由提供
|
||||
Reference in New Issue
Block a user