Vue 编译后该如何方便地嵌入 Go?

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.godist_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 buildgo run时执行。只能单独使用go generate执行。
其实 Go 里面还有很多类似//go:xxx这样的注释。像//go:noinline //go:nosplit //go:norace 这些,大多都是控制 Go 编译器的一些命令。

然后呢?togo!

回到 Drone,这里的go:generate togo http -package dist -output dist_gen.go后面的命令是使用了 Drone 作者自己开发的一个小工具togohttps://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 文件。

这里有个小坑...... 上面的这些配置都是在项目的 vue 分支下的,这才是目前 drone-ui 的主分支。master 分支已经两年没动了。服了。 当时疑惑了半天才发现。

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

再多聊几句

这是我目前找到的一个还算不错的解决方案。但我想这可能并不是最优雅的解决方法,因此本文的标题也是“方便地嵌入”,而不是“优雅地嵌入”。(笑)

唔…… 好像有一个多月没写东西了,有点怠惰了呀! 想保持着一个月两篇文章的节奏,但有时真的没啥有意义的可以拿来分享,强行尬写水文章还不如不写…… 这星期一直在宿舍复习准备期末考试,倦了就上上网听听歌。学新东西的计划打算放到考试之后了。

下一篇文章想必就是今年的年末总结了吧哈哈。