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 内部也是封装了个访问静态文件资源的函数,最终通过注册 GETHEAD 路由实现文件访问。

参照以上逻辑,自己实现静态资源访问,并对“/”结尾的请求特殊处理:

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 文件了。