一個可能提高開發(fā)效率的思路

這次就不聊地圖了啊。

對比前端前沿技術(shù),我好像花了更多時間在娛樂八卦、社會不公、游戲資訊、婚戀市場上面。不過我還是很上進(jìn)的,上班時間從未停止思考如何提高開發(fā)效率。

在過去,我基本上這么幾個思路(優(yōu)先級從高到低):
一、直接問產(chǎn)品,這個需求能不能不做——有時候真的可以不做,不行就轉(zhuǎn)向二;
二、把類似的代碼封裝更通用的代碼,承載以前、現(xiàn)在和未來可能出現(xiàn)的需求,這也是我一直思考的方向;
三、把類似的代碼復(fù)制一下,然后再上面改。大概是交付速度最快的,但有個比較突出的問題,就是在后續(xù)維護(hù)里面會獲得大量負(fù)面情緒。

最近翻看同事過去的代碼,找到另一個思路:就是盡量做好思路二,然后自動化實現(xiàn)思路三。我們知道,生成項目腳手架有很流行的工具yeoman,這個工具更多應(yīng)在新項目,這對我們來說,頻次太低,我更多的是基于現(xiàn)成項目去改東西。同事非常優(yōu)秀,想到了這點,用了粒度更細(xì)的node-plop——具體操作就是定義代碼文件的模板,經(jīng)過cli交互生成自定義的container、component等組件——這就比較貼近我們?nèi)粘5膱鼍?。我看了下,覺得模板不夠“業(yè)務(wù)”,而且,我為什么要每次操作都做一次問答題?

我最后省略了問答這一步,僅僅使用了handlebars模板引擎,也就是說,實質(zhì)上就是用一個模板引擎生成n個代碼文件,毫無技術(shù)含量,但應(yīng)該有用:對于我們長期toG或者toB的項目來說,前端開發(fā)無非就是實現(xiàn)表單、列表、對接口,而ant-design幾乎成為了這類場景的標(biāo)準(zhǔn)方案,所以我以這個為場景寫了一個例子。整個邏輯就是:
一、定義代碼生成的主要ts類型
1、數(shù)據(jù)模型字段的ts類型
2、傳入配置項的ts類型:包括該業(yè)務(wù)的各個字段、輸出列表時展示的字段、列表可查詢的字段、編輯表單時可編輯的字段

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、數(shù)據(jù)模型的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、數(shù)據(jù)模型的表單組件代碼
4、基于該數(shù)據(jù)模型的列表代碼
三、實現(xiàn)一些模板引擎helper,例如:
1、表單項處理,比如說input、select還是radio
2、列表項處理,比如說當(dāng)字段為常量時,以對應(yīng)的文本輸出

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

四、主流程實現(xiàn),就是根據(jù)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: "狀態(tài)",
      key: "status",
      selectOptions: [
        { label: "在職", value: 1 },
        { label: "失業(yè)", value: 2 },
        { label: "學(xué)生", value: 3 },
        { label: "退休", value: 4 },
      ],
      inputType: "select",
    },
    {
      label: "政治面貌",
      key: "politics",
      selectOptions: [
        { label: "群眾", value: 1 },
        { label: "團(tuán)員", 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;

然后拿著定義好的配置去執(zhí)行:

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

runGenerateTask(user, device);

然后執(zhí)行命令npm run generate,即可生成目標(biāo)文件,比如上面所見,我定義了兩個配置,就得出如下圖:

腳本執(zhí)行結(jié)果

隨便點開一個文件看看,編譯沒有提示報錯,感恩:


user的表單代碼文件

生成后文件后再對代碼進(jìn)行加工。這樣省略了一些無腦重復(fù)勞動。上面僅僅是一個例子,對于比較簡單的CRUD邏輯,接口文檔出來了,那前端代碼就基本出來80%了。成本的節(jié)約程度可能取決于怎么去設(shè)計代碼模板。我覺得這樣的好處是:
1、每個人都有自己最快最舒服的寫代碼方式,那就按自己的習(xí)慣去改造模板或者目錄生成邏輯。
2、這個思路雖然很土,但很容易落地,甚至可以一兩天內(nèi)落地,然后在未來節(jié)約一些時間成本,這個有待我去驗證。

想想,單憑人力手速,我已經(jīng)是石碣涌口村服第一駐場外包前端,現(xiàn)在還加入自動化元素,豈不是如虎添翼,真是未來可期啊。

最近“低代碼平臺”這個概念很火,也有可能已經(jīng)不火了,沒怎么留意。

低代碼平臺可能是一個更好的答案。但按我理解,它不是僅僅做了一個web應(yīng)用,而且要配套的是相應(yīng)的自定義“物料”的開發(fā)規(guī)范、大量的測試用例、迭代計劃、業(yè)務(wù)的系統(tǒng)對接流程、相應(yīng)的操作培訓(xùn)等等。我覺得最重要的是,需要專門的產(chǎn)品經(jīng)理角色專門去設(shè)計這個東西,這很可能是開發(fā)人員,但開發(fā)人員多少都會有點拒絕這么“業(yè)務(wù)”的工作。同時產(chǎn)生另一個問題,比如我要做一個東西,可能要先做好另一個東西,而這個前置的東西,業(yè)務(wù)邏輯復(fù)雜度可能是目標(biāo)的100倍,會讓領(lǐng)導(dǎo)焦慮。

所以我覺得,自研低代碼平臺這條路是曲折的,但同時是偉大的。在條路上很可能會衍生出其他用得上的副產(chǎn)品,開發(fā)人員的代碼組織\設(shè)計能力會得到提升,并且在理想情況下,落地之后會讓原本的生產(chǎn)流程發(fā)生變革,我想象的場景大概這樣:平臺對接了現(xiàn)有的一堆基礎(chǔ)服務(wù),然后按需求原型在界面拖放實現(xiàn),遇到新的業(yè)務(wù)邏輯,就按照既定的開發(fā)規(guī)范實現(xiàn)新的“物料”,再在界面拖放實現(xiàn)。簡單來說就是遇到一個需求,通過可視化功能實現(xiàn)90%,然后再通過寫代碼實現(xiàn)10%,因為大部份業(yè)務(wù)之前都驗證過,大大壓縮了測試時間。

但這個東西對我來說,成本實在太高了,我腦補一下,哪怕做出來,我都沒辦法告訴測試同事該怎樣測試,可能會有這么一種境況:開發(fā)的是一波前后端人員,測試的是另一波前后端人。我一駐場外包佬,能撬動的資源只能是我自己。。。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容