zoukankan      html  css  js  c++  java
  • golang 标准库template的代码生成

    curd-gen 项目

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

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

    自动生成的配置

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

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

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

    生成类型定义

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

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

    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的参数和返回值用到的对象就在上面自动生成的类型定义中。

    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 });
    }`
    

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

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

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

    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>
      );
    };`
    

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

    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 项目一起使用,但是其思路可以参考。

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

  • 相关阅读:
    asp.net 配置文件中HTTP错误号配置
    Vs .Net IDE 实用功能及常用快捷键
    Svcutil 的简单作用
    [JavaScript] 简单理解Ajax异步调用操作
    Frame套帧只显示纵向滚动条的实现
    [Design]设计模式之旅
    [Database] SqlServer: MSSQL SERVER 2005中数据乱码问题解决
    [Database] SqlServer: sql server 2005 收缩和备份数据库
    综合模糊查询 枫
    sql数据完整性 枫
  • 原文地址:https://www.cnblogs.com/wang_yb/p/15524632.html
Copyright © 2011-2022 走看看