简介
就在上周,Go 发布了 GO1.16 版本,此次更新带来了几个新特性,如 embed 原生支持,macos M1 处理器的支持,默认开启 go modules 等等。
在 embed 加入之前,go build 出来的二进制文件默认是不包括非代码文件的,比如开发一个 web 网站,要放到生产服务器运行,我们必须连同配置文件,html 资源文件一起上传到服务器执行,非常的难受。
当然社区也有一些方法实现将资源文件打包到编译后的二进制文件,但比较复杂,go1.16 直接引入 embed 解决了这个问题。今天便来实验下,顺便改造之前写的一个小工具。
实践
之所以写这个工具是因为,做开发的朋友经常会用到原型图,众多原型图工具都有打包成离线版本的功能,以供开发离线分享,但是离线文件传来传去实在是太麻烦了,于是想做一个 zip 上传,后台解压,提供在线版本的功能。
于是用 Gin 简单写了一个简易版本,就只有一个页面:
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>上传</title>
<!-- import Vue.js -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.6/vue.min.js"></script>
<!-- import stylesheet -->
<link href="https://cdn.bootcdn.net/ajax/libs/iview/3.5.5-rc.1/styles/iview.min.css" rel="stylesheet">
<!-- import iView -->
<script src="https://cdn.bootcdn.net/ajax/libs/iview/3.5.5-rc.1/iview.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
<style>
#app {
margin: 20px 40px;
}
</style>
</head>
<body>
<div id="app">
<h1>上传原型,获取永久在线浏览地址</h1>
<upload type="drag" action="/upload" paste="true" accept=".zip" :on-success="handleSuccess">
<div style="padding: 20px 0">
<icon type="ios-cloud-upload" size="52" style="color: #3399ff"></icon>
<p>点击或者拖拽上传</p>
</div>
</upload>
<alert type="success" v-for="url in zips">
浏览地址:${ url }
</alert>
<strong>历史prd:</strong>
<br><br>
<list border>
<list-item v-for="dir in dirs">
<a :href="dir.url" target="_blank">${ dir.name }</a>
</list-item>
</list>
</div>
<script>
new Vue({
el: "#app",
delimiters: ['${','}'],
data: {
zips: [],
dirs: [],
},
methods: {
handleSuccess(response,file,fileList){
this.zips.push(response.url)
}
},
created(){
axios.get("/dirs").then((res) => {
this.dirs = res.data.dirs
})
},
})
</script>
</body>
</html>
main.go:
package main
import (
"flag"
"github.com/gin-gonic/gin"
"log"
"net/http"
"path"
"prd/tools"
"strings"
)
func main() {
host := flag.String("host","127.0.0.1","Host")
port := flag.String("port","8080","Port")
uploadsDir := flag.String("uploadsDir","./uploads","上传文件存储地址")
wwwDir := flag.String("wwwDir","./www","www服务地址,即解压地址")
flag.Parse()
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
r.Static("/prd","./www")
r.LoadHTMLFiles("./views/index.html")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK,"index.html",nil)
})
r.POST("/upload", func(c *gin.Context) {
f,err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest,gin.H{
"error": err.Error(),
})
}
dst := path.Join(*uploadsDir,f.Filename)
err = c.SaveUploadedFile(f,dst)
if err != nil {
c.JSON(http.StatusInternalServerError,gin.H{
"error": err.Error(),
})
}
_,err = tools.Unzip(dst,*wwwDir)
if err != nil {
c.JSON(http.StatusInternalServerError,gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK,gin.H{
"msg": "upload success",
"url": path.Join("http://"+*host+":"+*port,"/prd/",strings.Split(f.Filename,".")[0],"/index.html"),
})
})
err := r.Run("0.0.0.0:" + *port)
if err != nil {
log.Fatal(err)
}
}
压缩文件目前仅支持了 zip 。
这样,go build 交叉编译成 linux 机器二进制文件,上传,执行,还得把 index.html 一起上传,体验很不好。
有了 embed 之后:
main.go:
package main
import (
"embed"
"flag"
"fmt"
"github.com/gin-gonic/gin"
"html/template"
"io/ioutil"
"log"
"net/http"
"path"
"prd/tools"
"strings"
)
//go:embed views/*
var f embed.FS
func main() {
host := flag.String("host","127.0.0.1","Host")
port := flag.String("port","8080","Port")
uploadsDir := flag.String("uploadsDir","./uploads","上传文件存储地址")
wwwDir := flag.String("wwwDir","./www","www服务地址,即解压地址")
flag.Parse()
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
r.Static("/prd","./www")
tmpl := template.Must(template.New("").ParseFS(f,"views/*"))
r.SetHTMLTemplate(tmpl)
r.GET("/", func(c *gin.Context) {
fmt.Println(tmpl.DefinedTemplates())
c.HTML(http.StatusOK,"index.html",nil)
})
r.POST("/upload", func(c *gin.Context) {
f,err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest,gin.H{
"error": err.Error(),
})
}
dst := path.Join(*uploadsDir,f.Filename)
err = c.SaveUploadedFile(f,dst)
if err != nil {
c.JSON(http.StatusInternalServerError,gin.H{
"error": err.Error(),
})
}
_,err = tools.Unzip(dst,*wwwDir)
if err != nil {
c.JSON(http.StatusInternalServerError,gin.H{
"error": err.Error(),
})
}
c.JSON(http.StatusOK,gin.H{
"msg": "upload success",
"url": strings.Join([]string{"http://"+*host+":"+*port,"/prd/",strings.Split(f.Filename,".")[0],"/index.html"},""),
})
})
r.GET("/dirs", func(c *gin.Context) {
dir,err := ioutil.ReadDir(*wwwDir)
if err != nil {
c.JSON(http.StatusInternalServerError,gin.H{
"error": err.Error(),
})
}
type Item struct {
Name string `json:"name"`
Url string `json:"url"`
}
var dirs []Item
for _,f := range dir {
if f.IsDir() && len(f.Name()) > 0 && f.Name() != "__MACOSX" {
dirs = append(dirs, Item{
Name: f.Name(),
Url: strings.Join([]string{"http://"+*host+":"+*port,"/prd/",f.Name(),"/index.html"},""),
})
}
}
c.JSON(http.StatusOK,gin.H{
"dirs": dirs,
})
})
err := r.Run("0.0.0.0:" + *port)
if err != nil {
log.Fatal(err)
}
}
打包(需用go1.16)之后,就可以不需要 index.html 了。
坑点
源于 html/template 不支持多级目录,即 template.New("").Parse** 系列仅用文件名标志不同的文件,index/index.html,user/index.html 这种结构的话,最终只能拿到最后那个 index.html ,暂时无解,只能 define 更改模板名称。
总结
embed 文档:https://golang.org/pkg/embed/
GO GO GO !
2021-02-24
有疑问加站长微信联系(非本文作者)