Web 开发过程中,不可避免会包含有 js
/ css
等静态资源文件,在 Gin 框架中如何优雅的访问这些静态资源呢?
一、访问外部静态文件
静态资源不打包进可执行文件内部,与可执行文件放在同一目录下,这时候 Gin 的 API 可以直接访问这些文件。
router := gin.Default()
router.Static("/admin", "resource/admin")
通过 http://127.0.0.1:8080/admin/
就可以访问 resource/admin
目录下的资源文件了。
但这种访问方式其实限制也比较多,如果我们希望通过不同的业务字段进行判断,进而实现不同资源文件的响应,那就需要自己增加 GET
方法进行实现。
router.GET("admin/*filePath", func(c *gin.Context) {
// 拿到请求 url
url := c.Request.RequestURI
// 这里可以通过 c.Param 等拿到参数,进行相关的业务判断,然后决定是否响应文件
// ...
// 响应文件
c.File("resource/" + c.Request.RequestURI)
})
二、访问内部静态文件
以上方式虽然简单好用,但是需要将资源文件放在可执行文件外部。在一些特殊场景下,我们可能希望把资源文件放在可执行文件内部。
这时候就需要借助 Embed
功能,这个功能是 Go 内置的静态文件打包工具需要 Go 1.16 版本以上才可以支持,只需要几行代码即可进行简单配置。
package main
import "embed"
//go:embed resource/admin/*
var f embed.FS
func main() {
router := gin.Default()
router.StaticFS("/admin", http.FS(f))
}
通过如上代码就可以将资源文件打包到可执行程序内,并通过 Gin 进行访问。但是,这个访问方式路径为 http://localhost:8080/admin/resource/admin/
在如上代码中加以修改,去掉 resource/admin/
路径:
package main
import "embed"
//go:embed resource/admin/*
var f embed.FS
func main() {
router := gin.Default()
st, _ := fs.Sub(f, "resource/admin")
router.StaticFS("/admin", http.FS(st))
}
上述代码中,静态资源文件访问方式路径为 http://localhost:8080/admin/
三、文件无法访问问题
可以发现通过上述方式访问静态文件时,子目录下文件名为 index.html
的文件无法正确访问。
这是因为 index.html
文件的访问都会被 301 重定向到对应的目录路径。
解决方法的话,小玖找了网上的一些教程,最终也没有个明确可行的方案。最终看了看 Gin,决定不使用 StaticFS
函数实现静态资源访问,仿照函数的逻辑自己实现。
关键部分源码:
// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead.
// Gin by default uses: gin.Dir()
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") {
panic("URL parameters can not be used when serving a static folder")
}
handler := group.createStaticHandler(relativePath, fs)
urlPattern := path.Join(relativePath, "/*filepath")
// Register GET and HEAD handlers
group.GET(urlPattern, handler)
group.HEAD(urlPattern, handler)
return group.returnObj()
}
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
absolutePath := group.calculateAbsolutePath(relativePath)
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
return func(c *Context) {
if _, noListing := fs.(*onlyFilesFS); noListing {
c.Writer.WriteHeader(http.StatusNotFound)
}
file := c.Param("filepath")
// Check if file exists and/or if we have permission to access it
f, err := fs.Open(file)
if err != nil {
c.Writer.WriteHeader(http.StatusNotFound)
c.handlers = group.engine.noRoute
// Reset index
c.index = -1
return
}
f.Close()
fileServer.ServeHTTP(c.Writer, c.Request)
}
}
通过以上源码可以看到,其实 StaticFS
内部也是封装了个访问静态文件资源的函数,最终通过注册 GET
和 HEAD
路由实现文件访问。
参照以上逻辑,自己实现静态资源访问,并对“/”结尾的请求特殊处理:
package main
import "embed"
//go:embed resource/admin/*
var f embed.FS
func main() {
router := gin.Default()
// 选中对应的子目录
st, _ := fs.Sub(f, "resource/admin")
fss := http.FS(st)
// 新建文件服务
fileServer := http.StripPrefix("/admin", http.FileServer(fss))
// 创建静态文件资源处理函数
handlerFunc := func(c *gin.Context) {
file := c.Param("filepath")
// 如果 / 结尾,则访问 index.html 文件
if strings.HasSuffix(file, "/") {
file = file + "index.html"
}
// Check if file exists and/or if we have permission to access it
fi, err := fss.Open(file)
if err != nil {
c.Writer.WriteHeader(http.StatusNotFound)
return
}
fi.Close()
fileServer.ServeHTTP(c.Writer, c.Request)
}
// Register GET and HEAD handlers
router.GET("/admin/*filepath", handlerFunc)
router.HEAD("/admin/*filepath", handlerFunc)
}
这样就可以通过路径直接访问 index.html
文件了。