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

这次就不聊地图了啊。

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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,717评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,501评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,311评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,417评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,500评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,538评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,557评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,310评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,759评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,065评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,233评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,909评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,548评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,172评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,420评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,103评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,098评论 2 352

推荐阅读更多精彩内容