golang 中使用 statik 将静态资源编译进二进制文件中,golang+vue,go generate,embed,web,macaron,beego

本文介绍了如何使用statik工具将前端Vue项目的静态资源打包进Go程序的二进制文件,以及如何利用goembed将资源编译到代码中,简化部署过程,并讨论了不同框架中的应用和适配方法。
摘要由CSDN通过智能技术生成

现在的很多程序都会提供一个 Dashboard 类似的页面用于查看程序状态并进行一些管理的功能,通常都不会很复杂,但是其中用到的图片和网页的一些静态资源,如果需要用户额外存放在一个目录,也不是很方便,如果能打包进程序发布的二进制文件中,用户下载以后可以直接使用,就方便很多。

最近在阅读 InfluxDB 的源码,发现里面提供了一个 admin 管理的页面,可以通过浏览器来执行一些命令以及查看程序运行的信息。但是我运行的时候只运行了一个 influxd 的二进制文件,并没有看到 css, html 等文件。

原来 InfluxDB 中使用了 statik 这个工具将静态资源都编译进了二进制文件中,这样用户只需要运行这个程序即可,而不需要管静态资源文件存放的位置。

一、statik

statik 命令参数

> statik -h
statik [options]

Options:
-src     The source directory of the assets, "public" by default.
-dest    The destination directory of the generated package, "." by default.

-ns      The namespace where assets will exist, "default" by default.
-f       Override destination if it already exists, false by default.
-include Wildcard to filter files to include, "*.*" by default.
-m       Ignore modification times on files, false by default.
-Z       Do not use compression, false by default.

-p       Name of the generated package, "statik" by default.
-tags    Build tags for the generated package.
-c       Godoc for the generated package.

-help    Prints this text.

Examples:

Generates a statik package from ./assets directory. Overrides
if there is already an existing package.

   $ statik -src=assets -f

Generates a statik package only with the ".js" files
from the ./public directory.

   $ statik -include=*.js

-p 参数指定生成的包名
-f 强制覆盖目标文件
-ns 生成带命名空间的fs,配合fs.RegisterWithNamespace 使用

statik 中的 fs.go主要代码

const defaultNamespace = "default"

// Register registers zip contents data, later used to initialize
// the statik file system.
func Register(data string) {
   RegisterWithNamespace(defaultNamespace, data)
}

// RegisterWithNamespace registers zip contents data and set asset namespace,
// later used to initialize the statik file system.
func RegisterWithNamespace(assetNamespace string, data string) {
   zipData[assetNamespace] = data
}

// New creates a new file system with the default registered zip contents data.
// It unzips all files and stores them in an in-memory map.
func New() (http.FileSystem, error) {
   return NewWithNamespace(defaultNamespace)
}

// NewWithNamespace creates a new file system with the registered zip contents data.
// It unzips all files and stores them in an in-memory map.
func NewWithNamespace(assetNamespace string) (http.FileSystem, error) {
   ......
}

dist 目录中的是打包好的 vue 项目,如下。

在这里插入图片描述

statik 可以将每个 vue 项目打包进一个go文件,如果你的项目中有多个 vue 项目需要打包,这里就需要有一个命名空间来区分访问的是哪个目录下的哪个文件,默认的命名空间是default

假设项目中有前台页面和后台页面,分别存放在 frontend/ditbackend/dit,现在需要将他们都使用 statik 打包进来。所以在浏览器中访问的时候也要显示的指明访问的是哪一块的资源文件,所以在 vue 打包的时候需要设置assetsPublicPath

build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/backend/dist',

这里, index.html中的 js, css 等的地址要带上assetsPublicPath前缀。

<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1">
<title>gocron - 定时任务系统</title><link href=/backend/static/css/app.ebfe035f57a51b3dbb6508c126d9d61a.css rel=
stylesheet></head><body><div id=app></div><script type=text/javascript src=/backend/static/js/manifest.047c13
3009965c5daaca.js></script><script type=text/javascript src=/backend/static/js/vendor.8798af468674508e4d9a.js>
</script><script type=text/javascript src=/backend/static/js/app.22dca5056b8b03f70aff.js></script></body></html>

安装 statik 包

go get -d github.com/rakyll/statik
go install github.com/rakyll/statik

首先要声明,一般在 main.go 文件中,这样直观一些,src 和 dest 都是相对于当前文件的路径:

package main

//go:generate statik -src=./backend/dist -dest=./include -p backend -ns backend -f
//go:generate statik -src=./frontend/dist -dest=./include -p frontend -ns frontend -f

func main() {
	......
}

go:generate指令需要手动执行,来到根目录下

go generate ./...

会默认生成

include/backend/statik.go
include/frontend/statik.go

statik.go文件内容为

// Code generated by statik. DO NOT EDIT.

package statik

import (
	"github.com/rakyll/statik/fs"
)

const Backend = "backend" // static asset namespace

func init() {
	data := ".............."
	fs.RegisterWithNamespace("backend", data)
}

将文件信息注册到 fs 之后,就可以实例化出来一个 http.FileSystem 对象。

注意,如果你没有使用命名空间,就需要以下划线_的形式手动引入 statik.go所在的包。

import (
	"resmirror/include/backend"
	"resmirror/include/frontend"
	"github.com/rakyll/statik/fs"
	...
)

var statikBackendFS http.FileSystem
var statikFrontendFS http.FileSystem

func init() {
	var err error
	statikBackendFS, err = fs.NewWithNamespace(backend.Backend)
	statikFrontendFS, err = fs.NewWithNamespace(frontend.Frontend)
	if err != nil {
		log.Fatal(err)
	}
}

最后就是使用 statikBackendFSstatikFrontendFS来读取文件内容了,注意,根目录是在dist内部的,比如

file, err := statikBackendFS.Open("/index.html")

这里返回的 file 类型是http.File,它服务于文件系统 FileSystem ,因此它是只读的,没有提供写的接口。

// A File is returned by a FileSystem's Open method and can be
// served by the FileServer implementation.
//
// The methods should behave the same as those on an *os.File.
type File interface {
	io.Closer
	io.Reader
	io.Seeker
	Readdir(count int) ([]fs.FileInfo, error)
	Stat() (fs.FileInfo, error)
}

还没完,你可能还记得,前面我们区分开了 backend 和 frontend,也就是说访问的时候是这样的

http://localhost/backend/dist/static/js/app.510882c2.js
http://localhost/fantend/dist/static/js/app.c26e62d2.js

而我们编译进来的 statik.go 文件系统却是如下形式 /static/js/app.510882c2.js,所以需要在路由上做一层处理。

不同的框架有不同的写法,比如 macaron框架可以将其注册到中间件中,而下面的Prefix选项就是设置路由前缀的,比如此处设置为backend/dist,这样,访问/backend/dist/static/js/app.510882c2.js就是在访问 statikBackendFS下的/static/js/app.510882c2.js

// 中间件注册
func RegisterMiddleware(m *macaron.Macaron) {
   ......
   m.Use(
   	macaron.Static(
   		"",
   		macaron.StaticOptions{
   			Prefix:     staticDir,
   			FileSystem: statikBackendFS,
   		},
   	),
   )
   ......
}

有的框架并没有支持statik FS,所以需要自己来实现,思路就是在路由那里做隐射。

以下是 beego 的写法

beego.InsertFilter("/admin/*", beego.BeforeStatic, func(ctx *context.Context) {
	if tools.InArray(ctx.Request.URL.Path, []string{"/admin", "/admin/", "/admin/index.html"}) {
		file, err := statikBackendFS.Open("/index.html")
		if err != nil {
			logs.Error("backend读取首页文件失败")
			http.NotFound(ctx.ResponseWriter, ctx.Request)
			return
		}

		io.Copy(ctx.ResponseWriter, file)
	}
})
beego.InsertFilter("/backend/dist/*", beego.BeforeStatic, func(ctx *context.Context) {
	realFile := strings.Replace(ctx.Request.URL.Path, "/backend/dist", "", 1)
	file, err := statikBackendFS.Open(realFile)
	if err != nil {
		logs.Error("backend目录下文件不存在:", realFile)
		http.NotFound(ctx.ResponseWriter, ctx.Request)
		return
	}

	// io.Copy(ctx.ResponseWriter, file)
	http.ServeContent(ctx.ResponseWriter, ctx.Request, realFile, time.Time{}, file)
})

这里将 io.Copy()换成了http.ServeContent()是有原因的,因为io.Copy()并没有设置Content-Type,因此默认值为text/plain,着导致浏览器并不会去解析 js, css 等文件,换成http.ServeContent()之后就你解决这个问题,它的说明如下

// ServeContent replies to the request using the content in the
// provided ReadSeeker. The main benefit of ServeContent over io.Copy
// is that it handles Range requests properly, sets the MIME type, and
// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
// and If-Range requests.
//
// If the response's Content-Type header is not set, ServeContent
// first tries to deduce the type from name's file extension and,
// if that fails, falls back to reading the first block of the content
// and passing it to DetectContentType.
// The name is otherwise unused; in particular it can be empty and is
// never sent in the response.
//
// If modtime is not the zero time or Unix epoch, ServeContent
// includes it in a Last-Modified header in the response. If the
// request includes an If-Modified-Since header, ServeContent uses
// modtime to decide whether the content needs to be sent at all.
//
// The content's Seek method must work: ServeContent uses
// a seek to the end of the content to determine its size.
//
// If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
//
// Note that *os.File implements the io.ReadSeeker interface.
func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker)

二、go embed (推荐)

除了使用 statik 之外,还有一种方式特别好用,golang 从 1.16 就开始支持 embed特性,你可以将文件或者目录编译到 go 的代码中。可以将目标资源编程成string, []byte, embed.FS三种类型。

//go:embed hello.txt
var s string
 
//go:embed hello.txt
var b []byte
 
//go:embed hello.txt
var f embed.FS
 
func main() {
 print(s)
 print(string(b))
 
 data, _ := f.ReadFile("hello.txt")
 print(string(data))
}

注意1,如果没有显示的调用embed这个包的话,则需要手动引入import _ "embed"

注意2,引入的资源要跟当前go文件在同一个目录中,也就是说它并不支持../的方式来指向外面的目录。

//go:embed hello1.txt hello2.txt
var f embed.FS
 
func main() {
 data1, _ := f.ReadFile("hello1.txt")
 fmt.Println(string(data1))
 
 data2, _ := f.ReadFile("hello2.txt")
 fmt.Println(string(data2))
}

指定目录

//go:embed helloworld
var f embed.FS
 
func main() {
 data1, _ := f.ReadFile("helloworld/hello1.txt")
 fmt.Println(string(data1))
 
 data2, _ := f.ReadFile("helloworld/hello2.txt")
 fmt.Println(string(data2))
}

另外,embed.FS调用Open()打开的文件并不支持http.ServeContent(),我们需要把embed.FS转换成http.FileSystem。使用http.FS(embedDist)方法。

还是以上面的项目为例。

main.go

//go:embed backend/dist frontend/dist
var embedDist embed.FS

routers.EmbedDistFS = http.FS(embedDist)

这里与 statik 的不同在于,编译后文件的访问路径要带上目录前缀,比如/backend/dist/index.html/frontend/dist/index.html

router.go

var EmbedDistFS http.FileSystem

beego.InsertFilter("/backend/dist/*", beego.BeforeStatic, func(ctx *context.Context) {
	realFile := ctx.Request.URL.Path
	file, err := EmbedDistFS.Open(realFile)
	if err != nil {
		logs.Error("backend目录下文件不存在:", realFile)
		http.NotFound(ctx.ResponseWriter, ctx.Request)
		return
	}

	http.ServeContent(ctx.ResponseWriter, ctx.Request, realFile, time.Time{}, file)
})

并不需要单独执行什么命令,go编译器自动检测 go:embed 指令并执行,方便很多。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值