模板渲染与前端分离: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)”。
笔记加载中…