一. 传统打包痛点

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文件类型

数据类型说明
[]byte表示数据存储为二进制格式,如果只使用[]byte和string需要以import (_ "embed")的形式引入embed标准库
string表示数据被编码成utf8编码的字符串,因此不要用这个格式嵌入二进制文件比如图片,引入embed的规则同[]byte
embed.FS表示存储多个文件和目录的结构,[]byte和string只能存储单个文件

2.2. 通配符

通配符释义
?代表任意一个字符(不包括半角中括号)
*代表0至多个任意字符组成的字符串(不包括半角中括号)
[…]和[!..]代表任意一个匹配方括号里字符的字符,!表示任意不匹配方括号中字符的字符
[a-z]、[0-9]代表匹配a-z任意一个字符的字符或是0-9中的任意一个数字
**部分系统支持,*不能跨目录匹配,** 可以,不过目前个golang中和*是同义词

当前静态资源文件如下:

➜  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。

在这里插入图片描述

项目地址: https://gitee.com/molonglove/service-admin

gitee主页: https://gitee.com/molonglove

Logo

前往低代码交流专区

更多推荐