解析golang 标准库template的代码生成方法

时间:2021-11-22 18:29:00

curd-gen 项目

curd-gen 项目的创建本来是为了做为 illuminant 项目的一个工具,用来生成前端增删改查页面中的基本代码。

最近,随着 antd Pro v5 的升级,将项目进行了升级,现在生成的都是 ts 代码。这个项目的自动生成代码都是基于 golang标准库 template 的,所以这篇博客也算是对使用 template 库的一次总结。

自动生成的配置

curd-gen 项目的自动代码生成主要是3部分:

  • 类型定义:用于API请求和页面显示的各个类型
  • API请求:graphql 请求语句和函数
  • 页面:列表页面,新增页面和编辑页面。新增和编辑是用弹出 modal 框的方式。

根据要生成的内容,定义了一个json格式文件,做为代码生成的基础。 json文件的说明在:https://gitee.com/wangyubin/curd-gen#curdjson

生成类型定义

类型是API请求和页面显示的基础,一般开发流程也是先根据业务定义类型,才开始API和页面的开发的。 ​

自动生成类型定义就是根据 json 文件中的字段列表,生成 ts 的类型定义。模板定义如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const TypeDTmpl = `// @ts-ignore
/* eslint-disable */
 
declare namespace API {
  type {{.Model.Name}}Item = {
    {{- with .Model.Fields}}
    {{- range .}}
    {{- if .IsRequired}}
    {{.Name}}: {{.ConvertTypeForTs}};
    {{- else}}
    {{.Name}}?: {{.ConvertTypeForTs}};
    {{- end}}{{- /* end for if .IsRequired */}}
    {{- end}}{{- /* end for range */}}
    {{- end}}{{- /* end for with .Model.Fields */}}
  };
 
  type {{.Model.Name}}ListResult = CommonResponse & {
    data: {
      {{.Model.GraphqlName}}: {{.Model.Name}}Item[];
      {{.Model.GraphqlName}}_aggregate: {
        aggregate: {
          count: number;
        };
      };
    };
  };
 
  type Create{{.Model.Name}}Result = CommonResponse & {
    data: {
      insert_{{.Model.GraphqlName}}: {
        affected_rows: number;
      };
    };
  };
 
  type Update{{.Model.Name}}Result = CommonResponse & {
    data: {
      update_{{.Model.GraphqlName}}_by_pk: {
        id: string;
      };
    };
  };
 
  type Delete{{.Model.Name}}Result = CommonResponse & {
    data: {
      delete_{{.Model.GraphqlName}}_by_pk: {
        id: string;
      };
    };
  };
}`

除了主要的类型,还包括了增删改查 API 返回值的定义。 ​

其中用到 text/template 库相关的知识点有:

  1. 通过 **with **限制访问范围,这样,在 {{- with xxx}} 和 {{- end}} 的代码中,不用每个字段前再加 .Model.Fields 前缀了
  2. 通过 range 循环访问数组,根据数组中每个元素来生成相应的代码
  3. 通过 if 判断,根据json文件中的属性的不同的定义生成不同的代码
  4. 自定义函数 **ConvertTypeForTs **,这个函数是将json中定义的 graphql type 转换成 typescript 中对应的类型。用自定义函数是为了避免在模板中写过多的逻辑代码

生成API

这里只生成 graphql 请求的 API,是为了配合 illuminant 项目。 API的参数和返回值用到的对象就在上面自动生成的类型定义中。 ​

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
const APITmpl = `// @ts-ignore
/* eslint-disable */
import { Graphql } from '../utils';
 
const gqlGet{{.Model.Name}}List = ` + "`" + `query get_item_list($limit: Int = 10, $offset: Int = 0{{- with .Model.Fields}}{{- range .}}{{- if .IsSearch}}, ${{.Name}}: {{.Type}}{{- end}}{{- end}}{{- end}}) {
  {{.Model.GraphqlName}}(order_by: {updated_at: desc}, limit: $limit, offset: $offset{{.Model.GenGraphqlSearchWhere false}}) {
    {{- with .Model.Fields}}
    {{- range .}}
    {{.Name}}
    {{- end}}
    {{- end}}
  }
  {{.Model.GraphqlName}}_aggregate({{.Model.GenGraphqlSearchWhere true}}) {
    aggregate {
      count
    }
  }
}` + "`" + `;
 
const gqlCreate{{.Model.Name}} = ` + "`" + `mutation create_item({{.Model.GenGraphqlInsertParamDefinations}}) {
  insert_{{.Model.GraphqlName}}(objects: { {{.Model.GenGraphqlInsertParams}} }) {
    affected_rows
  }
}` + "`" + `;
 
const gqlUpdate{{.Model.Name}} = ` + "`" + `mutation update_item_by_pk($id: uuid!, {{.Model.GenGraphqlUpdateParamDefinations}}) {
  update_{{.Model.GraphqlName}}_by_pk(pk_columns: {id: $id}, _set: { {{.Model.GenGraphqlUpdateParams}} }) {
    id
  }
}` + "`" + `;
 
const gqlDelete{{.Model.Name}} = ` + "`" + `mutation delete_item_by_pk($id: uuid!) {
  delete_{{.Model.GraphqlName}}_by_pk(id: $id) {
    id
  }
}` + "`" + `;
 
export async function get{{.Model.Name}}List(params: API.{{.Model.Name}}Item & API.PageInfo) {
  const gqlVar = {
    limit: params.pageSize ? params.pageSize : 10,
    offset: params.current && params.pageSize ? (params.current - 1) * params.pageSize : 0,
    {{- with .Model.Fields}}
    {{- range .}}
    {{- if .IsSearch}}
    {{.Name}}: params.{{.Name}} ? '%' + params.{{.Name}} + '%' : '%%',
    {{- end}}
    {{- end}}
    {{- end}}
  };
 
  return Graphql<API.{{.Model.Name}}ListResult>(gqlGet{{.Model.Name}}List, gqlVar);
}
 
export async function create{{.Model.Name}}(params: API.{{.Model.Name}}Item) {
  const gqlVar = {
    {{- with .Model.Fields}}
    {{- range .}}
    {{- if not .NotInsert}}
    {{- if .IsPageRequired}}
    {{.Name}}: params.{{.Name}},
    {{- else}}
    {{.Name}}: params.{{.Name}} ? params.{{.Name}} : null,
    {{- end}}
    {{- end}}
    {{- end}}
    {{- end}}
  };
 
  return Graphql<API.Create{{.Model.Name}}Result>(gqlCreate{{.Model.Name}}, gqlVar);
}
 
export async function update{{.Model.Name}}(params: API.{{.Model.Name}}Item) {
  const gqlVar = {
    id: params.id,
    {{- with .Model.Fields}}
    {{- range .}}
    {{- if not .NotUpdate}}
    {{- if .IsPageRequired}}
    {{.Name}}: params.{{.Name}},
    {{- else}}
    {{.Name}}: params.{{.Name}} ? params.{{.Name}} : null,
    {{- end}}
    {{- end}}
    {{- end}}
    {{- end}}
  };
 
  return Graphql<API.Update{{.Model.Name}}Result>(gqlUpdate{{.Model.Name}}, gqlVar);
}
 
export async function delete{{.Model.Name}}(id: string) {
  return Graphql<API.Delete{{.Model.Name}}Result>(gqlDelete{{.Model.Name}}, { id });
}`

这个模板中也使用了几个自定义函数,GenGraphqlSearchWhere,GenGraphqlInsertParams,**GenGraphqlUpdateParams **等等。

生成列表页面,新增和编辑页面

最后一步,就是生成页面。列表页面是主要页面:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
const PageListTmpl = `import { useRef, useState } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import { Button, Modal, Popconfirm, message } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns } from '@ant-design/pro-table';
import ProTable from '@ant-design/pro-table';
import { get{{.Model.Name}}List, create{{.Model.Name}}, update{{.Model.Name}}, delete{{.Model.Name}} } from '{{.Page.ApiImport}}';
import {{.Model.Name}}Add from './{{.Model.Name}}Add';
import {{.Model.Name}}Edit from './{{.Model.Name}}Edit';
 
export default () => {
  const tableRef = useRef<ActionType>();
  const [modalAddVisible, setModalAddVisible] = useState(false);
  const [modalEditVisible, setModalEditVisible] = useState(false);
  const [record, setRecord] = useState<API.{{.Model.Name}}Item>({});
 
  const columns: ProColumns<API.{{.Model.Name}}Item>[] = [
    {{- with .Model.Fields}}
    {{- range .}}
    {{- if .IsColumn}}
    {
      title: '{{.Title}}',
      dataIndex: '{{.Name}}',
    {{- if not .IsSearch}}
      hideInSearch: true,
    {{- end}}
    },
    {{- end }}{{- /* end for if .IsColumn */}}
    {{- end }}{{- /* end for range . */}}
    {{- end }}{{- /* end for with */}}
    {
      title: '操作',
      valueType: 'option',
      render: (_, rd) => [
        <Button
          type="primary"
          size="small"
          key="edit"
          onClick={() => {
            setModalEditVisible(true);
            setRecord(rd);
          }}
        >
          修改
        </Button>,
        <Popconfirm
          placement="topRight"
          title="是否删除?"
          okText="Yes"
          cancelText="No"
          key="delete"
          onConfirm={async () => {
            const response = await delete{{.Model.Name}}(rd.id as string);
            if (response.code === 10000) message.info(` + "`" + `TODO: 【${rd.TODO}】 删除成功` + "`" + `);
            else message.warn(` + "`" + `TODO: 【${rd.TODO}】 删除失败` + "`" + `);
            tableRef.current?.reload();
          }}
        >
          <Button danger size="small">
            删除
          </Button>
        </Popconfirm>,
      ],
    },
  ];
 
  const addItem = async (values: any) => {
    console.log(values);
    const response = await create{{.Model.Name}}(values);
    if (response.code !== 10000) {
      message.error('创建TODO失败');
    }
 
    if (response.code === 10000) {
      setModalAddVisible(false);
      tableRef.current?.reload();
    }
  };
 
  const editItem = async (values: any) => {
    values.id = record.id;
    console.log(values);
    const response = await update{{.Model.Name}}(values);
    if (response.code !== 10000) {
      message.error('编辑TODO失败');
    }
 
    if (response.code === 10000) {
      setModalEditVisible(false);
      tableRef.current?.reload();
    }
  };
 
  return (
    <PageContainer fixedHeader header={{"{{"}} title: '{{.Page.Title}}' }}>
      <ProTable<API.{{.Model.Name}}Item>
        columns={columns}
        rowKey="id"
        actionRef={tableRef}
        search={{"{{"}}
          labelWidth: 'auto',
        }}
        toolBarRender={() => [
          <Button
            key="button"
            icon={<PlusOutlined />}
            type="primary"
            onClick={() => {
              setModalAddVisible(true);
            }}
          >
            新建
          </Button>,
        ]}
        request={async (params: API.{{.Model.Name}}Item & API.PageInfo) => {
          const resp = await get{{.Model.Name}}List(params);
          return {
            data: resp.data.{{.Model.GraphqlName}},
            total: resp.data.{{.Model.GraphqlName}}_aggregate.aggregate.count,
          };
        }}
      />
      <Modal
        destroyOnClose
        title="新增"
        visible={modalAddVisible}
        footer={null}
        onCancel={() => setModalAddVisible(false)}
      >
        <{{.Model.Name}}Add onFinish={addItem} />
      </Modal>
      <Modal
        destroyOnClose
        title="编辑"
        visible={modalEditVisible}
        footer={null}
        onCancel={() => setModalEditVisible(false)}
      >
        <{{.Model.Name}}Edit onFinish={editItem} record={record} />
      </Modal>
    </PageContainer>
  );
};`

新增页面和编辑页面差别不大,分开定义是为了以后能分别扩展。新增页面:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const PageAddTmpl = `import ProForm, {{.Model.GenPageImportCtrls}}
import { formLayout } from '@/common';
import { Row, Col, Space } from 'antd';
 
export default (props: any) => {
  return (
    <ProForm
      {...formLayout}
      layout="horizontal"
      onFinish={props.onFinish}
      submitter={{"{{"}}
        // resetButtonProps: { style: { display: 'none' } },
        render: (_, dom) => (
          <Row>
            <Col offset={10}>
              <Space>{dom}</Space>
            </Col>
          </Row>
        ),
      }}
    >
    {{- with .Model.Fields}}
    {{- range .}}
{{- .GenPageCtrl}}
    {{- end}}
    {{- end}}
    </ProForm>
  );
};`

页面生成中有个地方困扰了我一阵,就是页面中有个和 text/template 标记冲突的地方,也就是 {{ 的显示。比如上面的 submitter={{"{{"}} ,页面中需要直接显示 {{ 2个字符,但 {{ }} 框住的部分是模板中需要替换的部分。

所以,模板中需要显示 {{ 的地方,可以用 {{"{{"}} 代替。

总结

上面的代码生成虽然需要配合 illuminant 项目一起使用,但是其思路可以参考。

代码生成无非就是找出重复代码的规律,将其中变化的部分定义出来,然后通过模板来生成不同的代码。通过模板来生成代码,跟拷贝相似代码来修改相比,可以有效减少很多人为造成的混乱,比如拷贝过来后漏改,或者有些多余代码未删除等等。

到此这篇关于golang 标准库template的代码生成的文章就介绍到这了,更多相关golang 标准库template内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!

原文链接:https://www.cnblogs.com/wang_yb/p/15524632.html