前后端分离联调:静态托管 SPA、开发代理与路由兜底(NoRoute)

前后端分离后常见部署形态:构建出的 SPA(HTML/JS/CSS)由 Go 直接托管静态文件,/api/* 走接口;联调阶段则让前端开发服务器把请求代理到 Go。本章讲三种场景的接法。

1. Gin 托管 SPA 构建产物

前端打包后一般是 dist/ 目录,静态资源与入口 HTML 分开处理:

r := gin.Default()

// 1) 静态资源目录
r.Static("/assets", "./web/dist/assets")

// 2) 接口与健康检查
r.GET("/api/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"pong": true})
})

// 3) 其余非 API 路径回退到 index.html(SPA 路由交给前端)
r.NoRoute(func(c *gin.Context) {
    if strings.HasPrefix(c.Request.URL.Path, "/api/") {
        c.JSON(http.StatusNotFound, gin.H{"error": "接口不存在"})
        return
    }
    c.File("./web/dist/index.html")
})

要点:/api/ 前缀的请求不能回退到 HTML,否则前端 404 时拿到的是一份 index.html 而不是 JSON。

2. 开发代理:前端服务器转发到 Go

开发期用 Vite/Webpack dev server 起前端,把 /api 代理到后端,解决跨域与端口割裂:

// 前端工程 vite.config.js 示意
export default {
  server: {
    proxy: { '/api': 'http://127.0.0.1:8080' }
  }
}

前端代码里直接请求 /api/xxx,浏览器只看到一个端口。

3. 不走代理时的 CORS

没有代理层、直接跨域调用时,需要后端开 CORS(推荐官方 gin-contrib/cors),注意预检 OPTIONS:

// 手动最小示例:允许来源并放行预检
r.Use(func(c *gin.Context) {
    c.Header("Access-Control-Allow-Origin", "*") // 生产按白名单收紧
    if c.Request.Method == http.MethodOptions {
        c.AbortWithStatus(http.StatusNoContent)
        return
    }
    c.Next()
})

带 Cookie 的跨域请求要 Allow-Credentials 且来源不能是 *,细节以 gin-contrib/cors 官方文档为准。

4. 区分开发/生产资源来源

  • 开发:前端 dev server 代理,Go 只跑 API;
  • 生产:前端构建产物交给 Go 托管(或交给 nginx),Go 暴露 API 与静态文件。

用环境变量切换前端资源根目录,避免为环境改代码。

注意点

  • r.Static 是普通路由,须在 NoRoute 兜底之外被正常匹配命中。
  • SPA 的 history 路由(如 /user/123)刷新时必须由 NoRoute 返回 index.html,否则 404。
  • 代理与 CORS 二选一即可,同时启用反而增加排查成本。
  • 相对路径依赖进程工作目录,生产建议用绝对路径或 embed(见部署章节)。

小结

三件事各司其职:静态托管(Static + NoRoute 兜底)、开发代理(dev server 转发 /api)、跨域兜底(CORS)。生产上更常用「nginx 托管静态 + 反代 API」,部署章节会接着讲。

笔记加载中…