Vue 编译后该如何方便地嵌入 Go?
有一段时间没写点东西了。前一阵子事挺多的,又是重构助手阳光长跑后端,又是准备协会 Web 培训,上周又是 D^3CTF 线下。周五周六连续熬了两个通宵处理各种突发的问题,直到周日才在场地旁的沙发上睡了六个小时。 原本想写篇文章复盘一下 D^3CTF 的,但是思来想去好像都是些零零散散的琐事,没有可以单独拿出来讲的。还有就是些技术“机密”,不太好拿来公开分享哈哈。
今天难得有空在期末复习之余写了下自己之前开的一个坑—— Pie-Baker。https://github.com/wuhan005/Pie-Baker
这是给我的树莓派写的小玩意,同时也是用来学习 Go 的interface
goroutine
以及反射等这些知识的练手项目。之前对于这些东西实践的还是有些少。
我打算在 Pie-Baker 写一个网页端的控制界面。前端 Vue.js,后端 Go + Gin。那么问题就出现了,对于前后端分离的项目,我该怎样去管理前端、后端的代码,并最终将它们整合起来呢?
静态资源编译进入 Go 二进制?
关于上面这个问题,可能首先想到的是将 Vue 编译后在dist
文件夹中的 HTML、CSS、JS 静态文件使用 Golang 相关的库打包进入 Golang 的二进制中。比如 packr https://github.com/gobuffalo/packr 。但是仔细一想,这一般是应用在打包一些不常变动的页面模板、图片等资源。然而 Vue 每次编译后产生的静态文件,光文件名中的哈希值就不一样了。
同时,如果这么做的话,编译出的静态文件就会加入到项目 Golang 后端仓库的版本管理中。每次 Vue 编译后的静态文件都需要复制到后端仓库中,同时因为静态文件名中的哈希值每次都会变,导致整个代码变动历史会十分繁杂。
还是因为 Vue 要编译的问题,你不大可能将 Vue 和 Golang 的源码都放在同一个仓库里。因为如果这样的话,你在一个仓库里会出现既能看到package.json
webpack.config.js
,又能看到go.mod
——地鼠🐭与鱿鱼🦑同时出现的奇妙景象。
有什么推荐的方法吗?
为了避免上面提到的迷惑行为,我找到了一个貌似不错的解决办法,在这里分享一下。这就是目前 Drone CI 正在使用的办法。 Drone CI 是一个用 Go 编写的 CI/CD,而其 Web 前端正式是采用 Vue 开发。因此就会遇到上文中提到的问题,Drone 作者的解决方法十分值得参考。
通过在 GitHub 上阅读 Drone 关于 Web 端的源码(https://github.com/drone/drone/tree/master/web
),我们得知 Drone 的前端是单独放在另一个仓库中的https://github.com/drone/drone-ui。
跟进这个仓库,我们发现它与一般的 Vue 项目不同的是,它有dist
目录。这是 Vue 存放打包编译后文件的文件夹,一般来说是会被 .gitignore 给设置成忽略的。而这里的dist
目录里面有两个文件dist.go
和dist_gen.go
。
其中dist.go
的“代码”只有短短的两行:
package dist
//go:generate togo http -package dist -output dist_gen.go
到这里就要介绍一下go generate
命令了。
go generate
是什么?
go generate
是自 Go 1.4 版本后加入的一个命令,当运行此命令时,它将扫描与当前包相关的源代码文件,找出文件中所有包含//go:generate
的特殊注释,并将该注释后面的内容当做 shell 执行。
虽说是可以执行任意命令,但它往往被用作自动生成代码。为了避免自动生成的代码之后被人为修改,往往还会在生成的代码前面加上// Code generated by xxxx; DO NOT EDIT.
这样的提示。
//go:generate
并不会在go build
或go run
时执行。只能单独使用go generate
执行。//go:xxx
这样的注释。像//go:noinline
//go:nosplit
//go:norace
这些,大多都是控制 Go 编译器的一些命令。然后呢?togo!
回到 Drone,这里的go:generate togo http -package dist -output dist_gen.go
后面的命令是使用了 Drone 作者自己开发的一个小工具togo
。https://github.com/bradrydzewski/togo
它可以将 HTML、SQL、JSON 等静态文件转换成 Go 文件。使我们可以直接在项目的 Go 源码中调用这些文件。togo
就是个 CLI 程序,源码不多;大概看了下,基本流程就是将静态文件的内容进行简单的处理后(比如转义掉反引号)然后使用 Go 自带的模板语言template
包将静态文件内容以及文件信息插入到事先写好的 Go 代码模板中。
这个 Go 代码模板的代码也是挺妙的。这里以 HTML、CSS、JS 等文件的打包模板为例。/template/files/http.tmpl
Drone 作者在这里其实是通过实现 Go 源码fs.go
中的FileSystem
接口,进而实现了一个自己的文件系统。
比较特殊的是,这个文件系统不是从系统目录中读取文件信息,而是从定义的files
这个map
变量中获取。
func (fs *fileSystem) Open(name string) (http.File, error) {
name = strings.Replace(name, "//", "/", -1)
f, ok := fs.files[name]
if ok {
return newHTTPFile(f, false), nil
}
index := strings.Replace(name+"/index.html", "//", "/", -1)
f, ok = fs.files[index]
if !ok {
return nil, os.ErrNotExist
}
return newHTTPFile(f, true), nil
}
// Index of all files
var files = map[string]file{
"/index.html": {
data: file0,
FileInfo: &fileInfo{
name: "index.html",
size: 21,
modTime: time.Unix(1576590337, 0),
},
},
"/js/a.js": {
data: file1,
FileInfo: &fileInfo{
name: "a.js",
size: 43,
modTime: time.Unix(1576590451, 0),
},
},
}
可以看到files
变量中存储了我们文件的内容以及基本信息。
同时,在togo
源码中我还了解到,程序默认会将目录下的files
文件夹中的静态文件进行打包。同时我发现drone-ui
项目的vue.config.js
文件中设置了项目的导出目录:
outputDir: "dist/files"
就是dist/files
文件夹,同时在 .gitignore 中也有使用dist/files/*
来忽略该文件夹。
这么一来就没错了——Vue 将编译生成的文件放在dist/files
目录下,然后调用go generate
命令运行togo
将 Vue 生成的静态文件转换成 Go 文件。
将dist_gen.go
引入到项目中
在 drone-ui 的 README 中,作者讲述了应该怎样发布该前端:
go generate ./...
go install ./...
第一步就是我们上面介绍的go generate
,它将dist.go
中的//go:generate
命令执行,生成dist_gen.go
文件。第二步使用go install
将当前文件夹放到GOPATH/pkg
下,这样我们可以以包的形式,在后端项目中引入这个前端了!
像 Drone 后端源码:
package web
import (
"net/http"
"github.com/drone/drone-ui/dist"
......
既分离了前后端项目的源代码,又通过包的方式使得前端和后端能很好的结合,妙啊!
这里再提一下上面两个命令中的./..
这个用法。它可以遍历包括当前文件夹在内的所有子文件夹。
起初看到这个还以为是操作系统的特性,但谷歌搜了半天没搜到。最后还是在 Go 的源码文档里看到了对此的解释。
The "go list" subcommand lists the import paths corresponding to its arguments, and the pattern "./..." means start in the current directory ("./") and find all packages below that directory ("...")
这也是为什么我们可以在前端根目录下就能执行当前项目下所有的//go:generate
。
再多聊几句
这是我目前找到的一个还算不错的解决方案。但我想这可能并不是最优雅的解决方法,因此本文的标题也是“方便地嵌入”,而不是“优雅地嵌入”。(笑)
唔…… 好像有一个多月没写东西了,有点怠惰了呀! 想保持着一个月两篇文章的节奏,但有时真的没啥有意义的可以拿来分享,强行尬写水文章还不如不写…… 这星期一直在宿舍复习准备期末考试,倦了就上上网听听歌。学新东西的计划打算放到考试之后了。
下一篇文章想必就是今年的年末总结了吧哈哈。
喜欢这篇文章?为什么不打赏一下呢?