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

这次就不聊地图了啊。

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

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

最近翻看同事过去的代码,找到另一个思路:就是尽量做好思路二,然后自动化实现思路三。我们知道,生成项目脚手架有很流行的工具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%,因为大部份业务之前都验证过,大大压缩了测试时间。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容