这次就不聊地图了啊。
对比前端前沿技术,我好像花了更多时间在娱乐八卦、社会不公、游戏资讯、婚恋市场上面。不过我还是很上进的,上班时间从未停止思考如何提高开发效率。
在过去,我基本上这么几个思路(优先级从高到低):
一、直接问产品,这个需求能不能不做——有时候真的可以不做,不行就转向二;
二、把类似的代码封装更通用的代码,承载以前、现在和未来可能出现的需求,这也是我一直思考的方向;
三、把类似的代码复制一下,然后再上面改。大概是交付速度最快的,但有个比较突出的问题,就是在后续维护里面会获得大量负面情绪。
最近翻看同事过去的代码,找到另一个思路:就是尽量做好思路二,然后自动化实现思路三。我们知道,生成项目脚手架有很流行的工具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
,即可生成目标文件,比如上面所见,我定义了两个配置,就得出如下图:
随便点开一个文件看看,编译没有提示报错,感恩:
生成后文件后再对代码进行加工。这样省略了一些无脑重复劳动。上面仅仅是一个例子,对于比较简单的CRUD逻辑,接口文档出来了,那前端代码就基本出来80%了。成本的节约程度可能取决于怎么去设计代码模板。我觉得这样的好处是:
1、每个人都有自己最快最舒服的写代码方式,那就按自己的习惯去改造模板或者目录生成逻辑。
2、这个思路虽然很土,但很容易落地,甚至可以一两天内落地,然后在未来节约一些时间成本,这个有待我去验证。
想想,单凭人力手速,我已经是石碣涌口村服第一驻场外包前端,现在还加入自动化元素,岂不是如虎添翼,真是未来可期啊。
最近“低代码平台”这个概念很火,也有可能已经不火了,没怎么留意。
低代码平台可能是一个更好的答案。但按我理解,它不是仅仅做了一个web应用,而且要配套的是相应的自定义“物料”的开发规范、大量的测试用例、迭代计划、业务的系统对接流程、相应的操作培训等等。我觉得最重要的是,需要专门的产品经理角色专门去设计这个东西,这很可能是开发人员,但开发人员多少都会有点拒绝这么“业务”的工作。同时产生另一个问题,比如我要做一个东西,可能要先做好另一个东西,而这个前置的东西,业务逻辑复杂度可能是目标的100倍,会让领导焦虑。
所以我觉得,自研低代码平台这条路是曲折的,但同时是伟大的。在条路上很可能会衍生出其他用得上的副产品,开发人员的代码组织\设计能力会得到提升,并且在理想情况下,落地之后会让原本的生产流程发生变革,我想象的场景大概这样:平台对接了现有的一堆基础服务,然后按需求原型在界面拖放实现,遇到新的业务逻辑,就按照既定的开发规范实现新的“物料”,再在界面拖放实现。简单来说就是遇到一个需求,通过可视化功能实现90%,然后再通过写代码实现10%,因为大部份业务之前都验证过,大大压缩了测试时间。
但这个东西对我来说,成本实在太高了,我脑补一下,哪怕做出来,我都没办法告诉测试同事该怎样测试,可能会有这么一种境况:开发的是一波前后端人员,测试的是另一波前后端人。我一驻场外包佬,能撬动的资源只能是我自己。。。