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]” 写的很清楚:
代码语言:javascript复制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],帮助我们解决了这个问题,使用的方法也很简单:
代码语言:javascript复制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 的下载了:
代码语言:javascript复制go get github.com/soulteary/gin-static
如果你是全新使用,在你的在程序中添加下面的引用内容即可:
代码语言:javascript复制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):
代码语言:javascript复制├── embed
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── public
│ └── page
└── simple
├── go.mod
├── go.sum
├── main.go
└── public
└── index.html
基础使用
程序的基础使用,和之前社区版本的接口一致,如果我们想在程序中直接使用本地的静态文件:
代码语言:javascript复制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 内嵌文件系统(后者)。
这样可以确保我们在玩的时候,静态文件支持所见即所得的修改立即生效,下面是我个人喜欢的用法示例:
代码语言:javascript复制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