完善 Golang Gin 框架的静态中间件:Gin-Static

Golang
78
0
0
2024-03-24

Gin 是 Golang 生态中目前最受用户欢迎和关注的 Web 框架,但是生态中的 Static 中间件使用起来却一直很不顺手。

所以,我顺手改了它,然后把这个改良版开源了。

写在前面

soulteary/gin-static

Gin-static 的改良版,我开源在了 soulteary/gin-static[1],也发布在了 Go 软件包市场:pkg.go.dev/github.com/soulteary/gin-static[2],有需要可以自取。

提到改良优化,那么就不得不提 Go-Gin 和原版的 Gin-Static 对于静态文件的处理。

关于 Go-Gin 和 Gin 社区的静态文件处理

在 Gin 的官方文档中,关于如何使用 Gin 来处理“静态文件相关请求[3]” 写的很清楚:

func main() {
    router := gin.Default()
    router.Static("/assets", "./assets")
    router.StaticFS("/more_static", http.Dir("my_file_system"))
    router.StaticFile("/favicon.ico", "./resources/favicon.ico")

    // Listen and serve on 0.0.0.0:8080
    router.Run(":8080")
}

不过,这个例子中,官方只考虑到了静态资源都存放于二级目录,并且静态资源目录只存在静态资源的情况。

如果我们的静态资源需要使用 / 根目录,或者在静态目录所在的 /assets/* 中,存在需要 Golang后端程序要进行处理的“动态逻辑”,或者我们希望使用通配符来处理某些静态文件路由,这个玩法就失效了。而这个情况,在很多前端比较重的应用中非常常见,尤其是我们希望用 Golang 来优化 Node 或者纯前端实现的项目时。

这个问题在社区的反馈中有提到过,“#21,不能够在 / 根目录使用静态文件[4]”、“#360,通配符和静态文件冲突[5]”。

所以,在八年前 gin-contrib 社区出现了一个专注于处理静态程序的中间件:gin-contrib/static [6],帮助我们解决了这个问题,使用的方法也很简单:

package main

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

func main() {
  r := gin.Default()
  // ...
  r.Use(static.Serve("/", static.LocalFile("/tmp", false)))
  // ...
}

不过,当基础功能完备后,这个插件就陷入了沉睡状态,版本号停留在 0.0.1 直至现在。

时过境迁,Golang 的版本已经升到了 1.21,这个中间件中引用的一些软件也变的陈旧,甚至被废弃,社区中也挂起了一些很好的功能实现(比如,“#19,Go 原生文件嵌入实现[7]”),但是因为作者比较忙碌或者没有相同的痛点,所以 PR 一直未能合并。

在若干年后批判古早的代码毫无意义,所以我们就不扯出代码一行行审阅了,我个人认为相对靠谱的动作是帮助它解决问题。

在早些时候,《深入浅出 Golang 资源嵌入方案:前篇[8]》、《深入浅出 Golang 资源嵌入方案:go-bindata篇[9]》这两篇文章中,我提到过的 Golang 官方和社区排名靠前的资源嵌入方案,对于制作性能靠谱、方便分发的单文件应用非常有价值。

所以,结合社区里存在的 PR 提交(feat: Implement embed folder and a better organisation[10]),我提交了一个新的 PR(#46)[11],对之前的程序和 PR 实现的代码都做了一些完善,并且确保这个中间件测试覆盖率是 100%,使用起来能够更安心。

下载 gin-static 优化版

和其他社区软件一样,使用下面的一句话命令,可以完成 gin-static 的下载了:

go get github.com/soulteary/gin-static

如果你是全新使用,在你的在程序中添加下面的引用内容即可:

import "github.com/soulteary/gin-static"

// 或
import (
    static "github.com/soulteary/gin-static"
)

如果你已经使用了社区的 github.com/gin-gonic/gin-static 软件包,并且不想修改已有程序的引用和行为,那么我们可以用另外一种方法。

在你的 go.mod 文件中,我们应该能够看到类似下面的内容:

module your-project

go 1.21.2

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/gin-gonic/gin-static v0.0.1
)

我们只需要在 require 之前,添加一条依赖替换规则即可:

module your-project

go 1.21.2

replace (
    github.com/gin-gonic/gin-static v0.0.1 => github.com/soulteary/gin-static v0.0.5
)

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/gin-gonic/gin-static v0.0.1
)

完成内容添加后,我们执行 go mod tidy,完成依赖的更新即可。不论是哪一种使用方式,当你执行完命令后,我们就能够使用支持 Go 原生嵌入文件使用啦。

使用 gin-static 优化版

在项目的示例目录中[12],我提交了两个使用示例程序,分别包含“基础使用(simple)” 和 支持“文件嵌入”的例子(embed):

├── embed
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   └── public
│       └── page
└── simple
    ├── go.mod
    ├── go.sum
    ├── main.go
    └── public
        └── index.html

基础使用

程序的基础使用,和之前社区版本的接口一致,如果我们想在程序中直接使用本地的静态文件:

package main

import (
    "log"

    "github.com/gin-gonic/gin"
    static "github.com/soulteary/gin-static"
)

func main() {
    r := gin.Default()

    // 静态文件在默认根路径
    r.Use(static.Serve("/", static.LocalFile("./public", false)))

    // 其他路径 /other-place
    // r.Use(static.Serve("/other-place", static.LocalFile("./public", false)))

    r.GET("/ping", func(c *gin.Context) {
        c.String(200, "test")
    })

    // Listen and Server in 0.0.0.0:8080
    if err := r.Run(":8080"); err != nil {
        log.Fatal(err)
    }
}

实际使用过程中,我们还可以对根目录做一些额外的逻辑,使用 r.[Method] 来覆盖默认的静态文件路由:

// 将静态资源注册到根目录,使用本地的 Public 作为“数据源”
r.Use(static.Serve("/", static.LocalFile("public", false)))
// 允许添加其他的路由规则处理根目录
r.GET("/", func(c *gin.Context) {
  c.Redirect(http.StatusMovedPermanently, "/somewhere")
})

文件嵌入

在早些时候,《深入浅出 Golang 资源嵌入方案:前篇[13]》、《深入浅出 Golang 资源嵌入方案:go-bindata篇[14]》这两篇文章中,我提到过的 Golang 官方和社区排名靠前的资源嵌入方案,对于制作性能靠谱、方便分发的单文件应用非常有价值。

使用 gin-static 来处理嵌入文件非常简单,并且支持多种用法:

package main

import (
    "embed"
    "fmt"
    "net/http"

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

//go:embed public
var EmbedFS embed.FS

func main() {
    r := gin.Default()

    // Method 1: use as Gin Router
    // trim embedfs path `public/page`, and use it as url path `/`
    r.GET("/", static.ServeEmbed("public/page", EmbedFS))

    // OR, Method 2: use as middleware
    // trim embedfs path `public/page`, the embedfs path start with `/`
    r.Use(static.ServeEmbed("public/page", EmbedFS))

    // OR, Method 2.1: use as middleware
    // trim embedfs path `public/page`, the embedfs path start with `/public/page`
    r.Use(static.ServeEmbed("", EmbedFS))

    // OR, Method 3: use as manual
    // trim embedfs path `public/page`, the embedfs path start with `/public/page`
    // staticFiles, err := static.EmbedFolder(EmbedFS, "public/page")
    // if err != nil {
    //  log.Fatalln("initialization of embed folder failed:", err)
    // } else {
    //  r.Use(static.Serve("/", staticFiles))
    // }

    r.GET("/ping", func(c *gin.Context) {
        c.String(200, "test")
    })

    r.NoRoute(func(c *gin.Context) {
        fmt.Printf("%s doesn't exists, redirect on /\n", c.Request.URL.Path)
        c.Redirect(http.StatusMovedPermanently, "/")
    })

    // Listen and Server in 0.0.0.0:8080
    r.Run(":8080")
}

上面的代码中,我们首先使用 //go:embed public 将本地的 public 目录读入 Golang 程序中,转换为程序可以访问的对象。然后你就可以根据你自己的具体情况,使用上面程序中的任意一种用法了。

当我们使用 go build 构建程序后,就能够得到一个包含了所有依赖静态文件的单一可执行文件啦。

个人倾向用法

我个人在使用的过程中,倾向于将上面两种用法合并在一起,当我们在开发的时候,使用本地文件系统(前者),而当我们构建的时候,则使用 Go 内嵌文件系统(后者)。

这样可以确保我们在玩的时候,静态文件支持所见即所得的修改立即生效,下面是我个人喜欢的用法示例:

if debugMode {
    r.Use(static.Serve("/", static.LocalFile("public", false)))
} else {
    r.NoRoute(
        // 例如,对存在的具体目录进行一些特殊逻辑处理
        func(c *gin.Context) {
            if c.Request.URL.Path == "/somewhere/" {
                c.Data(http.StatusOK, "text/html; charset=utf-8", []byte("custom as you like"))
                c.Abort()
            }
        },
        static.ServeEmbed("public", EmbedFS),
    )
    // 或者,不需要额外处理和拦截存在的静态文件
    // r.NoRoute(static.ServeEmbed("public", EmbedFS))
}

在上面的代码里,我们将本地的静态文件,在开发时默认挂载在 / 根目录,用于“兜底访问(fallback)”,这些文件允许被各种其他的路由覆盖。当我们进行构建或设置 debugMode=false 的时候,我们将静态文件挂载低优先级的 NoRoute 路由中,用于“兜底访问(fallback)”,如果我们需要调整或覆盖一些真实存在的静态文件,那么我们需要在路由前做额外的处理。

最后

好了,这个中间件就是这么简单,我们已经聊完了 80% 相关的内容啦。有机会我们在聊聊更有趣的 Embed 文件优化的故事。

--EOF

引用链接

[1] soulteary/gin-static: https://github.com/soulteary/gin-static

[2] pkg.go.dev/github.com/soulteary/gin-static: https://pkg.go.dev/github.com/soulteary/gin-static

[3] 静态文件相关请求: https://gin-gonic.com/docs/examples/serving-static-files/

[4] #21,不能够在 / 根目录使用静态文件: https://github.com/gin-gonic/gin/issues/75

[5] #360,通配符和静态文件冲突: https://github.com/gin-gonic/gin/issues/360

[6] gin-contrib/static : https://github.com/gin-contrib/static/commit/b0491c78a9ed6170dc00e31890d8d19374023194

[7] #19,Go 原生文件嵌入实现: https://github.com/gin-contrib/static/issues/19

[8] 深入浅出 Golang 资源嵌入方案:前篇: https://soulteary.com/2022/01/15/explain-the-golang-resource-embedding-solution-part-1.html

[9] 深入浅出 Golang 资源嵌入方案:go-bindata篇: https://soulteary.com/2022/01/16/explain-the-golang-resource-embedding-solution-go-bindata.html

[10] feat: Implement embed folder and a better organisation: https://github.com/gin-contrib/static/pull/20

[11] 一个新的 PR(#46): https://github.com/gin-contrib/static/pull/46

[12] 示例目录中: https://github.com/soulteary/gin-static/tree/main/example

[13] 深入浅出 Golang 资源嵌入方案:前篇: https://soulteary.com/2022/01/15/explain-the-golang-resource-embedding-solution-part-1.html

[14] 深入浅出 Golang 资源嵌入方案:go-bindata篇: https://soulteary.com/2022/01/16/explain-the-golang-resource-embedding-solution-go-bindata.html