這次就不聊地圖了啊。
對比前端前沿技術(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)文件,比如上面所見,我定義了兩個配置,就得出如下圖:

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

生成后文件后再對代碼進(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ā)的是一波前后端人員,測試的是另一波前后端人。我一駐場外包佬,能撬動的資源只能是我自己。。。