一、前言
在上一篇文章中,介绍了小工具【代码生成工具】中的任务1【解析数据字典文件】实现过程,在实现过程中对Go语言进行了学习,具体学习的内容如下:
- 搭建Go语言编写环境
- 基本框架结构及编译运行方式
- 在控制台打印信息的方式
- 定义参数的方式
- if 的使用方法
- 循环的使用方法
- 函数的定义与使用方法
- 第三方模块的使用方法
- xlsx文件的读取方法(excelize)
- 接收命令行参数的实现方式(flag)
- 结构体的使用方法
- error的定义与使用方法
- 去掉首尾空字符的方法( strings.TrimSpace)
- Json格式的使用方法(jsoniter)
- 将文本输出到文件中(ioutil)
我们已经生成了schema.json文件,接下来的任务就是将模板加载进来,然后在解析这个schema.json的过程中完成实际代码的生成。
二、任务2、3【加载模板、生成代码文件】的实现
1、基础准备
审视一下之前的代码,有如下发现:
- 已经完成了一个相对完整的子功能
- 根据不同的功能拆分出来了子函数
- 代码都存在于main.go文件中,文件行数已经比较多了
那么,在开始新任务之前,最好是将基于不同的函数功能,将单文件拆分成多文件,以增加可读性。新功能的代码,便可以直接再新的对应子文件中进行编写了。
拆分后的文件如下(终端中执行,➜ a_code_generator 是当前目录):
➜ a_code_generator tree main
main
├── console.go
├── json.go
├── main.go
├── xlsx.go
└── xlsx2schema.go
每个文件中包含的内容介绍如下:
- console.go
- receiveConsoleParam // 接收控制台变量
- json.go
- printJSON // 把 json 打印出来
- writeWithIoutil // 写入文件
- xlsx.go
- 结构体定义
- DataDict // 数据字典
- Collection // 数据集合
- Field // 数据字段
- listAllSheet // 输出 xlsx 文件中所有的 sheet 页 信息
- setFieldInfo // 将对应表头的内容设置到Field对应的属性上
- setCollectionField // 为集合扩充字段
- addDataDictIntoSlice // 将数据字典加入到集合中
- 结构体定义
- xlsx2schema.go
- analyzeSheet // 遍历xlsx文件中某sheet页逐行逐单元格
- xlsx2schema // xlsx 内容 转换成 schema 文件
- main.go
- main // 入口函数
斜体与加粗的部分是比上一篇文章中有所改变的方法
main.go 中的 main 方法改造后的结果,如下:
package main
// 入口函数
func main() {
println("程序运行 @ 开始")
// 1.接收控制台变量
fileName, sheetName, sheetIndex, err := receiveConsoleParam()
if err != nil {
println(err.Error())
return
}
// 2.xlsx 文件转换成 schema 文件
xlsx2schema(fileName, sheetName, sheetIndex)
println("程序运行 @ 结束")
}
函数xlsx2schema的方法定义如下:
// xlsx 内容 转换成 schema 文件
func xlsx2schema(fileName string, sheetName string, sheetIndex int) {
// 1.打开xlsx文件
f, err := excelize.OpenFile(fileName)
if err != nil {
println(err.Error())
return
}
// 2.如果 sheetName 为空 或 sheetIndex 为默认值,则打印出该文件的所有sheet页信息
if sheetName == "" && sheetIndex == -1 {
listAllSheet(f)
return
}
// 3.对xlsx文件中的指定名称的sheet页逐行逐单元格进行遍历
var rows [][]string
if sheetName != "" { // 3.1.当sheet页名称设置时,以 sheetName 为准
rows = f.GetRows(sheetName)
} else { // 3.2.当sheet页名称未设置时,以 sheetIndex 为准
rows = f.GetRows(f.GetSheetName(sheetIndex))
}
err = analyzeSheet(rows)
if err != nil {
println(err.Error())
return
}
}
将单文件拆分成多文件之后,程序的运行命令也需要做相应的改变,如下(终端中执行,➜ a_code_generator 是当前目录):
➜ a_code_generator go run main/* -f ./resource/datadict.xlsx -i 2
这里涉及到的知识点如下:
- Go语言同一package下的多文件实际上和一个文件一样,便量、函数等均可以随意访问
- 由于分成了多个文件,在运行或编译main.go时,一定要保证包内的其他文件也都进行了编译,如 go run main/*
- 程序运行,必须有一个main包,运行时会去main包下找main入口方法,因此,对于可执行的Go程序,main方法在main包下应该只有一个且不一定非要在main.go文件中
2、开始动手
step_01:设计模板文件
模板文件实际上就是有两部分组成的,分别是静态内容和动态内容。静态内容是固定不变的部分,动态内容则是在实际生成过程中需要进行替换的部分,这一部分就是模板中的变量,一般会使用专门的模板语言进行编写,由专门的模板引擎进行处理。通过搜索,发现了三个具有参考性的文章,如下:
- blog.csdn.net/darjun/arti…
- blog.csdn.net/TDCQZD/arti…
- blog.csdn.net/u012386544/…
- www.jianshu.com/p/29c9f5e06…
- www.jianshu.com/p/05671bab2…
- [docs.studygolang.com/pkg/text/te…](docs.studygolang.com/pkg/text/te… template definitions)
通过阅读文章,得知Go语言给我们提供了两个库来处理模板(text/template和html/template),我们要处理的模板是NodeJs的文本,使用 text/template 即可。
模板示例如下(model.tmpl):
'use strict';
const path = require("path");
const mongoose = require('mongoose');
const CommonDao = require('./commondao.js')
const dbconn = require('./index.js');
const ObjectId = mongoose.Schema.Types.ObjectId;
class Model extends CommonDao {
constructor(model) { super(model); }
getSchema() { return Schema; }
getConn() { return dbconn.mongoConn; }
getCollect() {
let file = __filename.split(path.sep);
file = file[file.length - 1].split('.')[0];
return file;
}
}
cont Schema = Model.SchemaExt({
{{.}}
});
module.exports = new Model(null);
最后面的 {{.}} ,便是要替换的内容,只有这一处,因此,没有用不同的变量标识。点"."代表当前作用域的当前对象。
step_02:创建代码文件 template.go
step_03:在 template.go 中添加解析 schema 的方法
schema文件已经在前一步骤中生成出来了,接下来使用程序将其用程序打开读取文件内容(json),并将内容反向解析成 数据字典集合,然后在集合遍历中进行处理即可。代码框架如下:
// 分析模式文件
func analyzeSchema(schemaPath string, modelTmplPath string, controllerTmplPath string) error {
// 1.加载 模式文件
schema, err := ioutil.ReadFile(schemaPath)
if err != nil {
return err
}
// 2.转换 模式文件到 数据字典集合中
var dataDictSlice []DataDict // 数据字典集合
if err := Json.Unmarshal(schema, &dataDictSlice); err != nil {
return err
}
// 3.加载模板文件
modelTmpl, _ := template.ParseFiles(modelTmplPath)
controllerTmpl, _ := template.ParseFiles(controllerTmplPath)
// 4.遍历数据字典集合
for _, dataDict := range dataDictSlice {
// 3.1.生成 model 代码片段
if err := generateModelFile(modelTmpl, dataDict); err != nil {
return err
}
// 3.2.生成 controller 代码片段
if err := generateControllerFile(controllerTmpl, dataDict); err != nil {
return err
}
}
// 5.没有出错,返回 nil
return nil
}
在遍历过程中,用 generateModelFile 和 generateControllerFile 承载了生成代码文件的职责。为了能够复用模板文件,需要将模板文件再遍历前提前加载。analyzeSchema 中涉及到的知识点如下:
- 使用 ioutil.ReadFile 读取 schema.json
- 使用 Json.Unmarshal 将 json 文本解析为 strcut对象 集合
- 使用 template.ParseFiles 加载模板文件(text/template)
- 遍历 结构体数组
step_04:在 template.go 中实现 generateModelFile
在 step_01 中可以看到 model.tmpl 的内容,需要填充的部分就是数据字典下各个字段的定义,因此,我们只需要在程序中遍历数据字典的字段集合,并且根据约定的规则将所有的字段信息转换拼接成代码片段,然后通过模板引擎将其替换到相应的位置后,输出成文件即可。代码框架如下:
// 生成 model 文件
func generateModelFile(modelTmpl *template.Template, dataDict DataDict) error {
// 1.获取 fields
fields := dataDict.Fields
// 2.将 map 的 key 升序处理
var keys []int
for key := range fields {
iKey, err := strconv.Atoi(key)
if err != nil {
return err
}
keys = append(keys, iKey)
}
sort.Ints(keys)
// 3.顺序遍历并生成 model 代码块
var modelScript strings.Builder
for _, key := range keys {
field := fields[strconv.Itoa(key)]
// 使用 generateFieldCode 来解析并生成每一个 field 对应的代码
modelScript.WriteString(generateFieldCode(field))
}
// 4.创建代码文件
modelFile, err := os.Create("./dist/model/" + dataDict.Collection.Name + ".js")
if err != nil {
return nil
}
// 5.模板解析
modelTmpl.Execute(modelFile, modelScript.String())
// 6.关闭文件
defer modelFile.Close()
// 7.没有错误,则返回 nil
return nil
}
fields是个 map,是无序的,如果需要 顺序遍历,需要将 key 转存到数组中且进行排序后,通过遍历key数组一一获取 map 中的元素来实现。在遍历过程中,使用 generateFieldCode 来对字段基于约定的规则转换成相应的代码,不同的情况下,此方法的逻辑是不一样的,在此文中就不在叙述了。generateModelFile 中涉及到的知识点如下:
- map 的顺序遍历
- string类型转int类型 及 int类型转string类型
- strings.Builder 的使用方式
- 创建文件的方式
- 解析模板的方式
- defer的使用方法
step_05:在 template.go 中实现 generateFieldCode
不同的情况下,此方法的逻辑是不一样的,在此文中就不再叙述了。
step_06:在 template.go 中实现 generateControllerFile
此方法与 generateModelFile 是类似的,在此文中就不再叙述了。
step_07:在 main 方法中,调用 analyzeSchema ,把功能串接起来
三、总结
结合上一篇文章,到此处,我们已经实现了从读取 xlsx 格式的数据字典开始,到生成 schema 文件,到最后生成代码文件的完整过程。在这个过程中,边查边写的了解了Go语言的一些基本特性,虽然距离熟悉Go语言还相差甚远,但也收获了一个可用的代码生成工具,也算是一个好的开头了。