模板渲染与前端分离:html/template、SPA 托管
一句话结论:两种姿势要分清——后端模板渲染(html/template)适合页面内嵌数据的传统站点,模板默认自动 HTML 转义、天然防存储型 XSS;前后端分离则把 Gin 当纯 API 服务,前端打包产物(SPA 的 dist)交给 Gin 静态托管或直接丢给 nginx。两者并存时按路径前缀分流,并单独处理 SPA 的 history 路由回退。
要点
- Gin 渲染 HTML:r.LoadHTMLGlob("templates/**/*") 或更灵活的 r.SetHTMLTemplate(tmpl),handler 里 c.HTML(200, "index", gin.H{...})。
- html/template 是“上下文感知”转义:{{.Name}} 输出到 HTML 文本/属性/JS 里会被自动转义;只有显式包成 template.HTML 才当原始 HTML 输出,富文本场景要格外小心 XSS。
- 自定义模板函数:先设置 r.FuncMap 再 LoadHTMLGlob(模板解析时注册函数才生效)。
- 托管静态资源:r.Static("/assets", "./web/dist/assets")、单文件 c.File("index.html");用 embed 可把 dist 打进二进制,部署只出一个文件。
- SPA 回退:前端路由(Vue Router history 等)刷新时路径不带 .html,需要 NoRoute 兜底返回 index.html,但必须与 API 404 区分开。
SPA 回退示例
r := gin.Default()
r.Static("/assets", "./web/dist/assets")
// 页面路由回退:非 /api 前缀的未知路径都交给 SPA
r.NoRoute(func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api") {
c.JSON(404, gin.H{"code": 404, "message": "not found"})
return
}
c.File("./web/dist/index.html") // 前端路由自行渲染
})
api := r.Group("/api/v1") // API 与静态资源天然分流
// embed 单二进制:把 dist 打进包内
//go:embed all:web/dist
var distFS embed.FS
sub, _ := fs.Sub(distFS, "web/dist")
r.StaticFS("/", http.FS(sub))
何时用哪种
| 场景 | 方案 | 原因 |
|---|---|---|
| 传统多页、服务端拼数据、SEO 敏感 | html/template 渲染 | 首屏快、利于爬虫,模板即代码 |
| 移动端/多端共用、交互复杂 | 纯 API + SPA 托管 | 前后端并行、可单独部署前端 |
| 生产环境 | 静态交给 nginx,Gin 只服务 /api | 静态资源缓存、gzip、TLS 都由 nginx 承担 |
追问记忆点
- 追问:SPA 刷新 404 怎么解决?——NoRoute 兜底返回 index.html,但注意不能吞掉 /api 的真实 404。
- 追问:为什么模板默认转义反而有人中招 XSS?——因为用了 template.HTML 或拼接 HTML 片段,等于手动关掉了转义。
- 记忆点:后端渲染看 html/template 的自动转义;前后端分离的关键是“/api 分流 + SPA 回退 + 静态层前置(nginx/embed)”。