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 的类型定义。模板定义如下:

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

这个模板中也使用了几个自定义函数,gengraphqlsearchwhere,gengraphqlinsertparams,**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 项目一起使用,但是其思路可以参考。

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

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