代码生成 vue-go

417 阅读6分钟

低代码入门

在go web项目开发过程中,遇到了一个需要重复去做的事情,那就是当添加了新的数据库表后,需要给对应的表添加结构体model映射,然后基本上每个表都会需要增删改查,以及router路由,这张表如果要在前端展示,还需要支持table展示页以及form编辑页。

如此大工作量的重复工作,每次功能开发或者复制其他相似页面,都会让人很烦躁。于是决定使用go template 模板引擎来完成代码的生成,可以理解是一次低代码的入门体验。

前提

  • 代码结构需要统一,也就是说代码生成模板尽量的保持通用性,可以直接复制使用。

    从代码结构看,我目前go后端所需生成的文件有5个。首先是router,生成增删改查等几个路由,然后是接口文件interfaces层以及接口实现handler层文件,然后是service层,最后就是model层对象映射。

  • 数据库表结构。所有的代码文件,前提是数据库表的存在,依赖表结构,生成对应的代码文件。遵循一切为了方便,在页面配置中可以添加表中没有的字段,或修改表字段基本信息。

最终的使用方式,先选择数据库接着选表名,剩下的表单根据表名自动填充。可以选择新增数据库中没有的

字段,也可以在操作一栏选择编辑字段相对应的信息。

我目前做的为低配版,生成的是代码文件的压缩包,需解压后自行复制使用,如果有需求,也可以做自动移动代码文件到对应目录。

  • 模板引擎文件处理。分为server和web两个,server目录下的文件上面介绍过。对于web目录的文件,因为我的前端项目使用的是vue2 + antd,所以在这三个文件中需要注意antd的组件语法。

api是接口层,table是列表的展示页,form是数据的编辑页。

开始

获取表结构

中心思想就是,根据数据库表结构,创建处理表的一些方法。

所以最少需要三个接口:

先获取数据库信息,选中所需数据库,接着获取表信息级联选择表,最后获取此表的具体结构,具体接口如下:

autoCodeRouter.GET("getDB", autoCodeApi.GetDB)            // 获取数据库
autoCodeRouter.GET("getTables", autoCodeApi.GetTables)    // 获取对应数据库的表
autoCodeRouter.GET("getColumn", autoCodeApi.GetColumn) 	  // 获取指定表所有字段信息

前端会根据表名,将_修改为驼峰样式,创建相对于的文件名称,结构体名称等。

并展示每个字段的具体信息,需要注意的是两列,搜索条件和字典。

字段名: go struct 字段,首字母大写
中文名: 字段名
字段json: go struct json 映射
字段数据类型: go struct 数据类型
数据库字段: 数据库中该字段
数据库字段类型: 数据库中该字段类型
数据库字段长度: 数据库中该字段长度
数据库字段描述: 数据库中该字段描述
搜索条件: 设置为搜索条件的字段,生产的前端页面支持字段的搜索
字典: 幽灵数据映射,比如sex字段,0为女,1为男,数据库保存的是0,1,但对于前端就需要男女的字典去映射,

特别的:系统会默认录入系统的字典,主要目的是go struct 类型映射。如下:

string: char, varchar, tinyblob, tinytext, text, blob, mediumblob, mediumtext, longblob, longtext
int: smallint, mediumint, int, bigint
bool: tinyint
float64: float, double, decimal
time.Time: date, time, year, datetime, timestamp

使用方式如下:

修改表信息

所有的出发点都是数据库表,一切都是为了方便,可以直接修改字段信息,此处的修改不是改变真实数据库信息,只会影响代码的生成使用。

所以针对某表,可以新增、修改、删除字段等操作,但最好是与原表保持一致,任何操作都需正确对应其表结构。

数据库表可以有以下操作:新增字段、编辑字段、删除字段、字段上移、字段下移

字段的上移与下移没有具体意义,只是为了排版好看,目前是根据数据库字段建立顺序展示。

新增字段: 可新增数据库中不存在的字段信息,但一般没有此需求。

编辑字段:修改字段数据,修改字段数据类型,相对应的修改了数据库字段的类型,为字段设置搜索条件(只有字符串类型可以设置搜索条件为like),只有整形字段可以添加关联字典等。

生成代码

修改想要的字段后,点击生成代码。

生成code.zip压缩包,解压后就是前后端所需要的文件,只需要复制到对应的项目目录。

主要过程

后端接受所要创建的表数据。

type AutoCodeStruct struct {
	StructName      string   `json:"structName"`      // Struct名称
	TableName       string   `json:"tableName"`       // 表名
	PackageName     string   `json:"packageName"`     // 文件名称
	HumpPackageName string   `json:"humpPackageName"` // go文件名称
	Abbreviation    string   `json:"abbreviation"`    // Struct简称
	Description     string   `json:"description"`     // Struct中文名称
	Fields          []*Field `json:"fields,omitempty"` // 表字段信息
}

// 判断字段是否为关键字,若是加上下划线
func KeyWord() {
	if token.IsKeyword(field) {
		field = field + "_"
	}
}

// 通过代理删除字段的左右空格
func TrimSpace(target interface{}) {
	t := reflect.TypeOf(target)
	if t.Kind() != reflect.Ptr {
		return
	}
	t = t.Elem()
	v := reflect.ValueOf(target).Elem()
	for i := 0; i < t.NumField(); i++ {
		switch v.Field(i).Kind() {
		case reflect.String:
			v.Field(i).SetString(strings.TrimSpace(v.Field(i).String()))
		}
	}
}

获取resource下所有的tpl文件

func GetAllTplFile(pathName string, fileList []string) ([]string, error) {
	files, err := ioutil.ReadDir(pathName)
	for _, fi := range files {
		if fi.IsDir() {
			fileList, err = GetAllTplFile(pathName+"/"+fi.Name(), fileList)
			if err != nil {
				return nil, err
			}
		} else {
			if strings.HasSuffix(fi.Name(), ".tpl") {
				fileList = append(fileList, pathName+"/"+fi.Name())
			}
		}
	}
	return fileList, err
}

生成*Template, 填充 template 字段

通过template.ParseFiles生成template,此处需要注意的是,在我的form.vue.tpl文件中有个自定义的函数用来做数字计算,所以需要单独的以template.New方式创建template,且需要注意的是New(name)的name需要与文件名保持一致。

// 定义函数sub,减法
// 获取切片长度减一
func TemplateSub(a, b int) int {
	return a - b
}

if file == "web/form.vue.tpl" {
	template, err = template.New("form.vue.tpl").Funcs(template.FuncMap{"sub": TemplateSub}).ParseFiles(path)
} else {
	template, err = template.ParseFiles(path)
}
if err != nil {
	return err
}

创建文件夹以及文件。

创建整个template文件夹,并命名为结构体的名字,创建server、web文件夹。

func PathExists(path string) (bool, error) {
	fi, err := os.Stat(path)
	if err == nil {
		if fi.IsDir() {
			return true, nil
		}
		return false, errors.New("存在同名文件")
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

func CreateDir(dirs ...string) (err error) {
	for _, v := range dirs {
		exist, err := PathExists(v)
		if err != nil {
			return err
		}
		if !exist {
			global.GLOBAL_LOG.Debug("create directory" + v)
			if err := os.MkdirAll(v, os.ModePerm); err != nil {
				global.GLOBAL_LOG.Error("create directory"+v, zap.Any(" error:", err))
				return err
			}
		}
	}
	return err
}

写入文件,生成文件

将所有文件压缩打包,并移除中间文件

// 生成文件
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0755)
if err != nil {
	return err
}
if err = value.template.Execute(f, data); err != nil {
    return err
}
_ = f.Close()

// 压缩打包
func ZipFiles(filename string, files []string, oldForm, newForm string) error {
	newZipFile, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer func() {
		_ = newZipFile.Close()
	}()
	zipWriter := zip.NewWriter(newZipFile)
	defer func() {
		_ = zipWriter.Close()
	}()
	// 把files添加到zip中
	for _, file := range files {
		err = func(file string) error {
			zipFile, err := os.Open(file)
			if err != nil {
				return err
			}
			defer zipFile.Close()
			info, err := zipFile.Stat()
			if err != nil {
				return err
			}
			header, err := zip.FileInfoHeader(info)
			if err != nil {
				return err
			}
			header.Name = strings.Replace(file, oldForm, newForm, -1)
			header.Method = zip.Deflate
			writer, err := zipWriter.CreateHeader(header)
			if err != nil {
				return err
			}
			if _, err = io.Copy(writer, zipFile); err != nil {
				return err
			}
			return nil
		}(file)
		if err != nil {
			return err
		}
	}
	return nil
}

// 移除中间文件
if err := os.RemoveAll(autoPath); err != nil {
	return
}

压缩包传给前端,并删除压缩包

func (autoApi *AutoCodeApi) CreateTemp(c *gin.Context) {
    ...
	c.Writer.Header().Add("Content-Disposition", fmt.Sprintf("attachment; filename=%s", "code.zip"))
	c.Writer.Header().Add("Content-Type", "application/json")		
    c.Writer.Header().Add("success", "true")
	c.File("./code.zip")
	_ = os.Remove("./code.zip")
}
const blob = new Blob([data])
const fileName = 'code.zip'
if ('download' in document.createElement('a')) {
  // 不是IE浏览器
  const url = window.URL.createObjectURL(blob)
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', fileName)
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link) // 下载完成移除元素
  window.URL.revokeObjectURL(url) // 释放掉blob对象
} else {
  // IE 10+
  window.navigator.msSaveBlob(blob, fileName)
}

最终效果