前言

在构建现代 Web 应用时,Vue.js 等流行的前端框架为我们提供了构建单页应用(SPA)的能力。 这些 SPA 应用通常依赖于前端路由来实现页面的无缝切换。

前端路由主要有两种模式:Hash 模式和 History 模式。

Hash 模式的 URL 格式如 https://example.com/admin#/abc/abc History 模式的 URL 格式则为 https://example.com/admin/abc/abc

尽管 Hash 模式简单易用,但 History 模式提供了更清晰的 URL 结构,更符合 SEO 优化和用户体验的需求。

然而,当 SPA 应用与 Golang 后端服务结合时,我们可能会遇到一个问题:SPA单页应用的前端路由如果使用的是 History模式 ,在浏览器端刷新页面后会报 404,这个的原因是因为SPA应用的路由只是注册在前端的项目的,当浏览器端刷新页面了,请求被发到了服务端来了,然后服务端没有这个页面,所以返回的是404页面。 那么常见的解决方法就是只能在前端的SPA应用中把路由模式改成 Hash 模式。

解决方法

这个问题如何解决呢?解决方法还是很多的,可以从服务器程序层解决,也可以从Nginx等反向代理程序端解决。

这里我们就是要在 Golang 端解决这个问题,在 Golang 端来解决这个前端路由的 url 重写的,把SPA的路由 url 全部重新回来入口的 index.html 文件即可.

这里我使用的是 go-gin 框架,既然前端是路由的问题,那么同理的我们要兼容也是在路由层解决。 假设我们的接口的前缀是 /api/v1/xxxx ,前端管理入口的uri前缀是:/admin。 那么在 Go-Gin 框架 中有一个 NoRoute 方法,假设我们的后台入口地址是:https://example.com/admin

r := gin.New()

//后台的入口
r.GET("/admin", func(ctx *gin.Context) {
	ctx.HTML(200, "admin", nil)
})

//兼容处理,把指定前缀的url做重定向后台回来到入口来
r.NoRoute(func(ctx *gin.Context) {
		if strings.Contains(ctx.Request.URL.Path, "/admin") {
			file, _ := os.ReadFile("template/admin/index.html")
			etag := fmt.Sprintf("%x", md5.Sum(file))
			ctx.Header("ETag", etag)
			ctx.Header("Cache-Control", "no-cache")
			
			// 如果客户端已经缓存了文件,则返回 304 不修改状态
			if match := ctx.GetHeader("If-None-Match"); match != "" {
				if strings.Contains(match, etag) {
					ctx.Status(http.StatusNotModified)
					return
				}
			}
			
			ctx.Data(http.StatusOK, "text/html; charset=utf-8", file)
			return
		}

      // 如果不是指定前缀的 URL,则返回 404 页面
		ctx.HTML(200, "notfound", nil)
	})

如果是其他的框架呢?该怎么处理? 这样也好处理,有类型 NoRoute 这种空路由方法的可以直接使用。如果没有的,那么中间件总有吧? 我们直接在中间件层来判断 uri 的前缀,如果是 /admin 前缀的,我们之间输出入口的代码

其他方式

当然,我们也不必完全拘泥于 Golang端,这个东西在 http 服务程序端解决也一样的 如服务端接口和前端分开部署: 部署2套代码,2个请求地址 服务端接口:https://api.example.com/api/v1 前端页面:https://admin.example.com/ 然后在前端的部署的项目中,配置一个 Url 重写规则,也能解决。

nginx 的配置:

location / {
  try_files $uri $uri/ /index.html;
}

Apache的配置:

<IfModule mod_negotiation.c>
  Options -MultiViews
</IfModule>

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>