一. 传统打包痛点

Golang作为api接口服务非常方便,日常将Gin的项目打包是二进制文件直接部署是很方便。但是作为前段的Vue或者React项目需要在nginx或者tomcat转发才可以。

这样对于一些中小项目就很麻烦,尤其我们写一下小工具。如果在使用前后端分离通过nginx转发代理部分,就很麻烦,失去了golang项目的便捷性。

二. golang新特性

在golang的1.16版本增加了embed的标签,肯方便我们将静态资源文件打包为二进制文件。这样有了原生支持就可以省去引入一些第三方关于静态资源处理的包了。

下面简单介绍一下这个新特性。

package main

import (
    _ "embed"
    "fmt"
)

//go:embed test.txt
var test string

func main() {
    fmt.Printf("测试文件内容: %s\n", test)
}

2.1. 支持的数据类型

在embed中,可以将静态资源文件嵌入到三种类型的变量,分别为:字符串、字节数组、embed.FS文件类型

import (_ "embed")

2.2. 通配符

****

当前静态资源文件如下:

➜  static tree
.
├── 1.log
├── 2.txt
└── imgs
    ├── 1.png
    ├── a1.jpg
    ├── a2.jpg
    ├── 2.png
    ├── 3.png
    ├── a1.png
    └── a2.png

1 directory, 7 files
//go:embed static/imgs
static/imgs下面所有的文件

//go:embed static/imgs/1.png
static/imgs下面的1.png文件

//go:embed static/imgs/*.jpg
static/imgs下面的所有的jpg文件

//go:embed static/imgs/a?.png
static/imgs下面的a1.png/a2.png

//go:embed *
static目录

2.3. 注意点

//go:embed//

embed只在使用在包中,不能写在函数中。下面这样写就是不对的

package main

import (
    _ "embed"
    "fmt"
)

func main() {
    //go:embed test.txt
    var test string
    fmt.Printf("测试文件内容: %s\n", test)
}

2.4. 使用场景

  • 项目中的初始化脚本配置文件我们可以直接打包到二进制文件中

  • 静态文件服务器,已将go代码和html打包在一起

  • 模板文件,例如tpl模板之类,原来只能内联。

三. Vue或者React打包

下面开始我们重头戏,如何将我们build好的Vue或者React的项目和golang的web框架gin打包到一起。

这里我们需要重新实现一个接口:

type FS interface {
  // Open opens the named file.
  //
  // When Open returns an error, it should be of type *PathError
  // with the Op field set to "open", the Path field set to name,
  // and the Err field describing the problem.
  //
  // Open should reject attempts to open names that do not satisfy
  // ValidPath(name), returning a *PathError with Err set to
  // ErrInvalid or ErrNotExist.
  Open(name string) (File, error)
}

这个接口golang的fs中提供的。这也是golang的http服务中加载静态资源时候调用的接口;但是这个接口因为加载路径的文件,需要我们重写一下,不然在通过embed引入静态资源之后因为路径问题无法获取到embed.FS的中文件。

在这里插入图片描述

上面是我项目中react打包之后的路径。在外面我们增加一个html.go的文件。

package resources

import "embed"

//go:embed html/index.html
var Html []byte

//go:embed html/static
var Static embed.FS

这里面的html是为了我们后面渲染index.html提供的变量。

核心部分:

package initialization

import (
  "embed"
  "errors"
  "github.com/gin-gonic/gin"
  "io/fs"
  "net/http"
  "path"
  "path/filepath"
  "server-admin-api/resources"
  "strings"
)

type Resource struct {
  fs embed.FS
  path string
}

func NewResource() *Resource {
  return &Resource{
    fs: resources.Static,
    path: "html",
  }
}

func (r *Resource) Open(name string) (fs.File, error) {
  if filepath.Separator != '/' && strings.ContainsRune(name, filepath.Separator) {
    return nil, errors.New("http: invalid character in file path")
  }
  fullName := filepath.Join(r.path, filepath.FromSlash(path.Clean("/static/" + name)))
  file, err := r.fs.Open(fullName)

  return file, err
}

func InitResource(engine *gin.Engine) *gin.Engine {
  engine.StaticFS("/static", http.FS(NewResource()))
  return engine
}

这里注意这一行代码:

fullName := filepath.Join(r.path, filepath.FromSlash(path.Clean("/static/" + name)))

因为我们通过gin的路由获取指定的资源加载的时候是无法带上static的前缀,或者可能前缀和static不一样。这里就需要重写一个Open打开文件的接口将里面获取对应静态资源文件路径改写。

接着在handler中增加对index.html首页的渲染操作,但是这里我们还需要解决一个刷新之后404的问题,所以在加一个重定向的接口,找不到就重定向到我们渲染index.html的页面。

package api

import (
  "github.com/gin-gonic/gin"
  "net/http"
  "server-admin-api/resources"
)

type HtmlHandler struct {}

func NewHtmlHandler() *HtmlHandler {
  return &HtmlHandler{}
}

// RedirectIndex 重定向
func (h *HtmlHandler) RedirectIndex(c *gin.Context) {
  c.Redirect(http.StatusFound, "/ui")
  return
}

func (h *HtmlHandler) Index(c *gin.Context) {
  c.Header("content-type", "text/html;charset=utf-8")
  c.String(200, string(resources.Html))
  return
}

最后将静态资源处理的路由加入到路由分组中:

func InitRouter(engine *gin.Engine, fs embed.FS, flag bool) *gin.Engine {
  // 跨域
  engine.Use(middleware.Cors())
  // 静态资源加载
  resourceRouter(engine)
  // 其他路由
  group := engine.Group("v1")
  {
    groupRouter(group)     // 服务分组接口
    customizeRouter(group) // 自定义服务接口
    websocketRouter(group)
  }
  return engine

}

// resourceRouter 静态资源配置
func resourceRouter(engine *gin.Engine) {
  html := api.NewHtmlHandler()
  group := engine.Group("/ui")
  {
    group.GET("", html.Index)
  }
  // 解决刷新404问题
  engine.NoRoute(html.RedirectIndex)
}

四. 项目链接

如果有不明白的地方可以到我的gitee主页查看这个项目的源码。欢迎点个star。

在这里插入图片描述