0%

gin 部署前后端分离项目

最近在写一个自己的小项目 GONEList,技术栈是 gin+vue。准备在部署的时候,如何使用 gin 来同时部署前后端?
把前端使用 / 进行挂载,后端监听其他的路由,这样会遇到问题,默认的路由规则不允许同时挂载到 staticfs 到根和监听其他路由
在 google 之后 issue 里面找到 gin 的一个库 gin-contrib/static 来解决,直接调用即可

背景

前端是 Sillywa 使用 vue+iView 写的 gonelist-web,由于 Golang 的跨平台特性,在发布的时候想要尽量减少部署的难度,于是想用 gin 来同时路由前端生成的 dist 文件夹。

根据官方文档上的 router.StaticFS() 来进行的话会遇到报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
router.StaticFS("/", http.Dir("my_file_system"))
router.Run(":8080")
}
build-fail

static-middleware

由于 gin 本身的路由会发生冲突,gin-contrib/static 使用中间件的方式判断是否存在该静态文件,使用起来非常简单,下面是例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)

func main() {
router := gin.Default()
router.Use(static.Serve("/", static.LocalFile("./dist/", false)))

router.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})

router.Run(":8080")
}

fileserver 是一个 Handler,判断 fs 中是否有请求的文件,如果有则使用 fileserver 将文件写入到 gin 的 writer 里面返回,否则就不处理,中间件处理结束之后就会去匹配对应的 api。戳我跳转代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// Static returns a middleware handler that serves static files in the given directory.
func Serve(urlPrefix string, fs ServeFileSystem) gin.HandlerFunc {
fileserver := http.FileServer(fs)
if urlPrefix != "" {
fileserver = http.StripPrefix(urlPrefix, fileserver)
}
return func(c *gin.Context) {
if fs.Exists(urlPrefix, c.Request.URL.Path) {
fileserver.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
}
}

使用拓展

上面讲到的是对于挂载静态文件的解决方法,主要处理的问题是 gin 默认使用 httprouter 不支持路由的优先级
通过使用中间件来判断不同的 URL 应该走什么样的逻辑,如果你的某条冲突的路由是需要执行自己的某个 api,可以自己写一个中间件来解决问题

假设现在有一个 appV1 是一个 Group,路由为 /apps,在 AppV1 下有两个 GET 接口(注意 GET 和 POST 是不会冲突的

  • /statistics 这个是自己逻辑
  • /:appid/users 获取某个 appid 的 users

此时如果直接这么写就会产生冲突

1
2
3
4
5
appV1 := r.Group("/apps")
appV1.GET("/:appid/users", func(c *gin.Context){...})
appV1.GET("/statistics", func(c *gin.Context){...})
// panic: 'statistics' in new path '/apps/statistics' conflicts with existing wildcard ':appid' in existing prefix '/apps/:appid'
// 原因在报错中写的很清楚,如果有一个 appid 的值为 statistics 那么就会出现二义性

我们可以通过写一个中间件来解决,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import "github.com/gin-gonic/gin"

// 自己写的逻辑函数
func Statistics(c *gin.Context) {
c.JSON(200, gin.H{
"url": c.Request.URL.Path,
})
}

func hand() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/apps/statistics" {
Statistics(c) // 这里执行 app.Statistics(c)
c.Abort()
}
}
}
func main() {
r := gin.Default()

appV1 := r.Group("/apps")
appV1.Use(hand())
appV1.GET("/:appid/users", func(c *gin.Context) {
appid, _ := c.Get("appid")
c.JSON(200, gin.H{
"appid": appid,
"api": "user",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

参考资料

听说好看的人都关注了我的公众号《泫言》