5

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

 8 months ago
source link: https://soulteary.com/2024/01/03/golang-gin-static-middleware-improves.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

2024年01月03日阅读Markdown格式6357字13分钟阅读

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

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

soulteary/gin-static

soulteary/gin-static

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

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

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

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

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,不能够在 / 根目录使用静态文件”、“#360,通配符和静态文件冲突”。

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

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 原生文件嵌入实现”),但是因为作者比较忙碌或者没有相同的痛点,所以 PR 一直未能合并。

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

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

所以,结合社区里存在的 PR 提交(feat: Implement embed folder and a better organisation),我提交了一个新的 PR(#46),对之前的程序和 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 优化版

在项目的示例目录中,我提交了两个使用示例程序,分别包含“基础使用(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 资源嵌入方案:前篇》、《深入浅出 Golang 资源嵌入方案:go-bindata篇》这两篇文章中,我提到过的 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 文件优化的故事。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK