当下正值五月,上海的天气还挺凉爽,老程序员的内心很焦虑,焦虑可能会失业。想想做点什么事儿吧,于是干点小活,写这篇文章。写点什么呢,就写前端组件标准化。这个课题是我三年前,在某虎任职的时候,我们的领导提出来的一个课题。
当时某虎大概有100+前端开发人员,这么大的队伍,归属不同的业务部门,不同的团队,但有一个统一的组织——大前端,网罗了web前端,移动端,小程序,部分nodejs。当然,web端的人数最多,提效就显得尤为重要。
组件标准化,核心是为了解决开发效率问题。我记得当时有说要搞一个拖拽式低代码平台,当时是不被大领导看好的,他说咱们先不说项目的可行性,我们最低的要求新人进入公司,要能够保证他学到知识和技能,走出公司能让人有一技之长。这其实是比较婉转的否决了拖拽式的项目,但是又不太好直截了当的说明。
我当时在某虎任职前端开发组长,团队有10个人左右。在上面提出标准化的同时,我们其实自己内部也在做这件事情,也在尝试解决效率问题。团队人数不算少,但当时每个人依旧很忙,几乎每天都要加班。作为组长,我有责任和义务帮助大家脱离苦海,虽然这只是一个美好愿景,但我确实付出了很多努力。也就是说我现在讲的这个项目,得益于那时候的理论积累和可行性的沉淀。
我们正式开始聊项目设计,他的模样是长的下面这个样子:
项目的代码结构,是以JSON的形式表达,内置了很多常用的组件,特别是表单组件足以覆盖绝大部分场景。代码最终是保存在数据库中。在这个系统里管理着【菜单,环境变量,业务代码,发布回滚】等操作。
一个简单的增删改查页面代码例子
{
baseInfo: {
systemName: 'CRUD页面'
},
data: {
list: [],
VIPOptions: [
{ label: '普通会员', value: 1 },
{ label: '高级会员', value: 2 }
]
},
page: [
{
type: 'FormSearch',
attrs: {
labelWidth: '80px'
},
formList: [
{
type: 'input',
label: '手机号',
field: 'phone'
},
{
type: 'input',
label: '用户昵称',
field: 'nickname'
},
{
type: 'select',
label: 'VIP',
field: 'vipLevel',
options: this.data.VIPOptions
},
{
type: 'datetimerange',
label: '注册时间',
field: 'datetimerange',
attrs: {
startField: 'startTime',
endField: 'endTime'
}
}
]
},
{
type: 'button',
label: ' + 新增',
click: async () => {
await this.$$dialog.open(this.$$renderModal, {
title: '新增',
refId: 'modal-form',
className: 'render-modal',
size: 'medium',
props: {
value: {}
}
});
}
},
{
type: 'button',
label: '导出',
click: async () => {
// 前端导出-CSV
let datalist = [];
this.data.list.forEach(item => {
let arr = [];
arr.push(item.id);
arr.push(item.nickname);
arr.push(item.phone);
datalist.push(arr);
});
this.$$utils.downloadCsv({
dataList: datalist,
headerArr: ['用户ID', '用户昵称', '手机号'],
fileName: '用户列表' + this.$$moment().getDate().toString()
});
}
},
{
type: 'Table',
attrs: {
isInitLoad: true,
pageParamNames: ['pageNum', 'pageSize'],
className: ['p-t-10'],
// true和false返回的数据结构不一样
showPagination: true,
loadData: async params => {
let url = SETTINGS.apiServer + '/setting/demo/user/page';
let { data } = await this.$$axios.post(url, params);
// 如果showPagination=false,只需要返回数组即可
// return data.list || []
this.data.list = data.list || [];
return {
list: data.list || [],
total: data.rowCount
};
}
},
tableList: [
{
prop: 'id',
label: '用户ID'
},
{
prop: 'nickname',
label: '用户昵称'
},
{
prop: 'avatar',
label: '用户头像',
renderType: 'img',
width: '120px'
},
{
prop: 'phone',
label: '手机号',
class: 'link',
click(v, data) {
this.$router.push(`/xx/xx?id=${data.id}`);
}
},
{
prop: 'price',
label: '价格',
renderType: 'htlm',
display(v) {
return `<strong>¥${v}</strong>`;
}
},
{
prop: 'addTime',
label: '注册时间',
renderType: 'datetime'
},
{
prop: 'vipLevel',
label: 'VIP',
mapList: this.data.VIPOptions
},
{
prop: 'opt',
label: '操作',
renderType: 'opt',
width: '100px',
list: [
{
type: 'button',
label: '编辑',
attrs: {
size: 'small',
type: 'text'
},
click: async data => {
this.$$dialog.open(this.$$renderModal, {
title: '编辑',
refId: 'modal-form',
className: 'render-modal',
size: 'medium',
vm: this,
props: {
value: JSON.parse(JSON.stringify(data))
}
});
}
},
{
type: 'button',
label: '删除',
attrs: {
size: 'small',
type: 'text'
},
click: data => {
this.$confirm('确定删除该记录吗?', '提示', {
type: 'warning'
}).then(async () => {
let url = SETTINGS.apiServer + '/setting/demo/user/remove';
await this.$$axios.post(url, { id: data.id });
this.$message({ message: '操作成功', type: 'success' });
this.reloadTable();
});
},
vif: rowData => rowData.vipLevel === 1
}
]
}
]
},
{
id: 'modal-form',
type: 'modal',
attrs: {
inline: false,
labelWidth: '90px'
},
onSave: async formData => {
let url = SETTINGS.apiServer + '/setting/demo/user/save';
await this.$$axios.post(url, formData);
this.$message({ message: '操作成功', type: 'success' });
// 刷新表格数据
this.reloadTable();
},
formList: [
{
type: 'input',
label: '用户昵称',
field: 'nickname',
attrs: { maxlength: 20 },
rules: { required: true }
},
{
type: 'imageUpload',
label: '图片上传',
field: 'avatar',
rules: { required: true },
clasName: ['full-width'],
attrs: {
tips: '支持图片格式png/jpg/jpeg,最大500KB',
format: ['.png', '.jpg', '.jpeg'],
maxSize: 500
}
},
{
type: 'select',
label: 'VIP',
field: 'vipLevel',
options: this.data.VIPOptions,
rules: { required: true }
},
{
type: 'input',
label: '手机号',
field: 'phone',
attrs: { maxlength: 11 },
rules: { required: true, mobile: true }
}
]
}
]
}
JSON表达的优点
- 代码规范,速度快效率高
- bug少
- 纯组件化开发,解决重复劳动
JSON表达的缺点 不灵活,交互方式单一,标准化之后就会必然产生这个结果
有人问,JSON配置如何实现复杂的交互控制?这是一个核心要解决的问题,很多人都会考虑的重点问题。我的解决方式很简单,把复杂的交互的东西都装进一个组件内完事,其实就是没有解决,我们要学会换位思考,向上思考尤为重要。在我们程序设计领域要学会一个最重要的思想:没有什么是加一层解决不了的事情。你比如设计模式,xx架构设计,都不知道加多少层了。
因此我们需要将组件简单的分类
- 通用组件(通用)
- 业务组件(特定通用)
- 不通用业务组件(不通用)
针对各种业务组件,JSON要保证有良好的注入机制。这样就能解决自定义的问题,真正的拥抱变化。
目前该项目还未开源,但是有一个简化版本针对表单配置的项目已经开源,这个vue-json-form是开源项目,json化配置表单,有兴趣可以看看。