美文网首页
一个可能提高开发效率的思路

一个可能提高开发效率的思路

作者: 黄永熙_ | 来源:发表于2022-07-28 02:51 被阅读0次

    这次就不聊地图了啊。

    对比前端前沿技术,我好像花了更多时间在娱乐八卦、社会不公、游戏资讯、婚恋市场上面。不过我还是很上进的,上班时间从未停止思考如何提高开发效率。

    在过去,我基本上这么几个思路(优先级从高到低):
    一、直接问产品,这个需求能不能不做——有时候真的可以不做,不行就转向二;
    二、把类似的代码封装更通用的代码,承载以前、现在和未来可能出现的需求,这也是我一直思考的方向;
    三、把类似的代码复制一下,然后再上面改。大概是交付速度最快的,但有个比较突出的问题,就是在后续维护里面会获得大量负面情绪。

    最近翻看同事过去的代码,找到另一个思路:就是尽量做好思路二,然后自动化实现思路三。我们知道,生成项目脚手架有很流行的工具yeoman,这个工具更多应在新项目,这对我们来说,频次太低,我更多的是基于现成项目去改东西。同事非常优秀,想到了这点,用了粒度更细的node-plop——具体操作就是定义代码文件的模板,经过cli交互生成自定义的container、component等组件——这就比较贴近我们日常的场景。我看了下,觉得模板不够“业务”,而且,我为什么要每次操作都做一次问答题?

    我最后省略了问答这一步,仅仅使用了handlebars模板引擎,也就是说,实质上就是用一个模板引擎生成n个代码文件,毫无技术含量,但应该有用:对于我们长期toG或者toB的项目来说,前端开发无非就是实现表单、列表、对接口,而ant-design几乎成为了这类场景的标准方案,所以我以这个为场景写了一个例子。整个逻辑就是:
    一、定义代码生成的主要ts类型
    1、数据模型字段的ts类型
    2、传入配置项的ts类型:包括该业务的各个字段、输出列表时展示的字段、列表可查询的字段、编辑表单时可编辑的字段

    export type FieldInputType = "input" | "select" | "radio";
    export type SelectOption = { label: string; value: string | number };
    export type Field = {
      key: string;
      label: string;
      inputType: FieldInputType;
      selectOptions?: SelectOption[];
      antdComponentName?: string;
      desc?: string;
    };
    export type Model = {
      baseUrl: string;
      displayName: string;
      fields: Field[];
      searchFields?: string[];
      tableFields?: string[];
      formFields?: string[];
    };
    
    

    二、定义代码模板文件:
    1、某些字段的枚举代码

    {{#each enumList}}
      export enum
      {{name}}
    {
      {{#each options}}
        {{label}}={{value}},
      {{/each}}
    }
    {{/each}}
    

    2、数据模型的ts定义代码、增删查改的请求代码

    //...
      import { 
    {{#each enumList}}
      {{name}},
    {{/each}} 
    } from './enum';
    
    export interface {{name}} {
      {{#each fieldList}}
        {{key}}:{{dataType}}; //{{desc}}
      {{/each}}
    }
    
    export type {{name}}ListSearchParams = Pick<{{name}},{{{tsPick searchFields}}}>;
    export type {{name}}ListItem = Pick<{{name}},{{{tsPick tableFields}}}>;
    
    export const get{{name}}SearchList = async (s: {{name}}ListSearchParams, page:number = 1, pageSize:number = 10) => {
      const url = getRequestUrl(`{{baseUrl}}/list`);
      const res = Request.post(url, { ...s, page,  pageSize});
      return { 
        list: getValidArray(res.data.data) as {{name}}ListItem[],
        total: getValidTotal(res.data.total),
      }
    }
    //...
    

    3、数据模型的表单组件代码
    4、基于该数据模型的列表代码
    三、实现一些模板引擎helper,例如:
    1、表单项处理,比如说input、select还是radio
    2、列表项处理,比如说当字段为常量时,以对应的文本输出

    ...
    registerHelper("renderTableColumn", (fields: Field[], enumMapping) => {
      const str = (fields || []).map((field) => {
        const currentEnumName = enumMapping[field.key];
        return `{dataIndex: "${field.key}", key: "${field.key}", title: "${
          field.label
        }", ${
          currentEnumName
            ? `
          render: v => ${currentEnumName}[v]
        `
            : ``
        } }`;
      });
      return str.join(",");
    });
    registerHelper("renderAntdComponents", (field: Field) => {
      switch (field.inputType) {
        case "input":
          return `<Input placeholder="请输入${field.label}"/>`;
        case "radio":
          return `<Radio.Group>
            ${field.selectOptions
              ?.map(
                (d) => `<Radio value="${d.value}">
              ${d.label}
            </Radio>`
              )
              .join("")}
          </Radio.Group>`;
        case "select":
          return `<Select placeholder="请选择${field.label}">
        ${field.selectOptions
          ?.map(
            (d) => `<Select.Option value="${d.value}">
          ${d.label}
        </Select.Option>`
          )
          .join("")}
      </Select>`;
      }
    });
    ...
    

    四、主流程实现,就是根据node的文件处理api搬砖。

    //...
    export default function doGenerate<T extends Model>(model: T) {
      const currentDir = model.displayName.toLocaleLowerCase();
      const currentOutput = path.join(outputPath, currentDir);
      if (!existsSync(outputPath)) {
        mkdirSync(outputPath);
      }
      if (!existsSync(currentOutput)) {
        mkdirSync(currentOutput);
      }
      const { enumMapping, enumList, fieldList } = generateEnumFile<T>(
        model,
        currentOutput,
        "enum.ts"
      );
      execTemplate(currentOutput, "service.ts", {
        enumList,
        name: model.displayName,
        fieldList,
        searchFields: model.searchFields,
        tableFields: model.tableFields,
        baseUrl: model.baseUrl,
      });
      const { searchFields, formFields, tableFields } = pickRelativeFieldList<T>(
        model
      );
      execTemplate(currentOutput, "table.tsx", {
        tableFieldList: tableFields,
        antdComponentsList: pickFormItemComponent(searchFields),
        searchFieldList: searchFields,
        name: model.displayName,
        enumMapping,
        enumNameList: _.values(enumMapping),
      });
      execTemplate(currentOutput, "form.tsx", {
        formFieldList: formFields,
        antdComponentsList: pickFormItemComponent(formFields),
        name: model.displayName,
        enumMapping,
        enumNameList: _.values(enumMapping),
      });
    }
    
    export const runGenerateTask = async <T extends Model>(...args: T[]) => {
      const total = args.length;
      const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
      args.forEach(async (model, i) => {
        doGenerate(model);
        i < total - 1
          ? process.stdout.write(`generate success: ${i + 1} / ${total} \r`)
          : process.stdout.write(
              `generate success: ${i + 1} / ${total} \n${generateFileList.join(
                "\n"
              )}`
            );
      });
      await sleep(1000);
    };
    
    //...
    

    然后按类型约束,定义一个传参,如下:

    import { Model } from "@/types";
    export default {
      displayName: "User",
      baseUrl: "/api/user",
      fields: [
        {
          label: "id",
          key: "id",
          inputType: "input",
        },
        {
          label: "性别",
          key: "gender",
          selectOptions: [
            { label: "男", value: 1 },
            { label: "女", value: 2 },
          ],
          inputType: "radio",
        },
        {
          label: "状态",
          key: "status",
          selectOptions: [
            { label: "在职", value: 1 },
            { label: "失业", value: 2 },
            { label: "学生", value: 3 },
            { label: "退休", value: 4 },
          ],
          inputType: "select",
        },
        {
          label: "政治面貌",
          key: "politics",
          selectOptions: [
            { label: "群众", value: 1 },
            { label: "团员", value: 2 },
            { label: "党员", value: 3 },
          ],
          inputType: "select",
        },
      ],
      tableFields: ["name", "address", "gender", "status", "politics"],
      searchFields: ["name", "status", "gender"],
      formFields: ["name", "address", "gender", "status", "politics"],
    } as Model;
    

    然后拿着定义好的配置去执行:

    import { runGenerateTask } from "./generate";
    import device from "./model/device";
    import user from "./model/user";
    
    runGenerateTask(user, device);
    

    然后执行命令npm run generate,即可生成目标文件,比如上面所见,我定义了两个配置,就得出如下图:

    脚本执行结果

    随便点开一个文件看看,编译没有提示报错,感恩:


    user的表单代码文件

    生成后文件后再对代码进行加工。这样省略了一些无脑重复劳动。上面仅仅是一个例子,对于比较简单的CRUD逻辑,接口文档出来了,那前端代码就基本出来80%了。成本的节约程度可能取决于怎么去设计代码模板。我觉得这样的好处是:
    1、每个人都有自己最快最舒服的写代码方式,那就按自己的习惯去改造模板或者目录生成逻辑。
    2、这个思路虽然很土,但很容易落地,甚至可以一两天内落地,然后在未来节约一些时间成本,这个有待我去验证。

    想想,单凭人力手速,我已经是石碣涌口村服第一驻场外包前端,现在还加入自动化元素,岂不是如虎添翼,真是未来可期啊。

    最近“低代码平台”这个概念很火,也有可能已经不火了,没怎么留意。

    低代码平台可能是一个更好的答案。但按我理解,它不是仅仅做了一个web应用,而且要配套的是相应的自定义“物料”的开发规范、大量的测试用例、迭代计划、业务的系统对接流程、相应的操作培训等等。我觉得最重要的是,需要专门的产品经理角色专门去设计这个东西,这很可能是开发人员,但开发人员多少都会有点拒绝这么“业务”的工作。同时产生另一个问题,比如我要做一个东西,可能要先做好另一个东西,而这个前置的东西,业务逻辑复杂度可能是目标的100倍,会让领导焦虑。

    所以我觉得,自研低代码平台这条路是曲折的,但同时是伟大的。在条路上很可能会衍生出其他用得上的副产品,开发人员的代码组织\设计能力会得到提升,并且在理想情况下,落地之后会让原本的生产流程发生变革,我想象的场景大概这样:平台对接了现有的一堆基础服务,然后按需求原型在界面拖放实现,遇到新的业务逻辑,就按照既定的开发规范实现新的“物料”,再在界面拖放实现。简单来说就是遇到一个需求,通过可视化功能实现90%,然后再通过写代码实现10%,因为大部份业务之前都验证过,大大压缩了测试时间。

    但这个东西对我来说,成本实在太高了,我脑补一下,哪怕做出来,我都没办法告诉测试同事该怎样测试,可能会有这么一种境况:开发的是一波前后端人员,测试的是另一波前后端人。我一驻场外包佬,能撬动的资源只能是我自己。。。

    相关文章

      网友评论

          本文标题:一个可能提高开发效率的思路

          本文链接:https://www.haomeiwen.com/subject/poytwrtx.html