低代码入门
在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)
}