背景
这几年做了一些后台系统,最多的场景就是内容管理页面,页中主要包括表单查询及各种管理操作。随着业务扩展,开发了不少类似的页面后,发现在相同项目中各个表单之间结构、风格、样式大致相同,每次新增页面都是采用 copy大法 再修改参数逻辑。这种写法容易产生大量的重复代码,于是想到了对组件进行封装,希望提高代码复用率,提升开发效率。
之前做的项目主要基于Vue2+ElementUI开发的,因此本文就在这个基础上发表一些对表单封装的设计与思考。
代码已上传至GitHub仓库(Form组件),欢迎star⭐⭐⭐
设计 & 实现
配置化表单
我们先来看下面两个表单案例,表单1:
查询
修改
删除
表单2:
查询
修改
删除
我们把表单分成两个部分,输入框项和按钮组。对比这两个表单来看,按钮组都是由三个按钮组成并且文本、type 都是相同的,不同的是表单1里的部分按钮带有icon。而对于输入框项也是同样,标签本质大多是相同的,不同的是一些配置,包括label
、placeholder
、clearable
......
我们尝试把这些配置抽取出来,生成一个 JSON 格式的数据,看看会发生什么?
先来看按钮组,一个按钮需要什么?文本、绑定方法、尺寸、样式...。根据elementUI按钮组件提供的props,可以把一个按钮描述成下面这样一个配置:
{ name: 'query', type: 'primary', text: '查询', icon: 'el-icon-search', size: 'small' }
由于按钮绑定的函数里可能涉及表单外的数据,因此决定为按钮配置一个唯一标识:name,我们可以让按钮点击时触发自定义事件并传出 name。
再来看看输入框项,一个输入框一般包括哪些属性?
- type 类型。比如 input、select、cascader(联级选择器)
- label 输入项的描述。
- name 字段名。
- value 输入项绑定的值。
- placeholder、size、clearable、disabled等其他配置。
在以上的配置基础上,我们用一个 JSON 数据描述一个表单:
{
inputs: [
{ type: 'input', name: 'name', label: '姓名', value: '' },
{
type: 'select', name: 'sex', label: '性别',
options: [{ label: '男', value: '1' }, { label: '女', value: '2' }],
value: ''
}
],
button: [
{
name: 'query',
type: 'primary',
text: '查询',
icon: 'el-icon-search'
},
{
name: 'delete',
type: 'danger',
text: '删除',
icon: 'el-icon-delete'
}
]
}
相比于前面用 template 编写的表单,这种 JSON 配置就显得更加清晰且好维护。我们还可以进一步优化,对于一些大量重复的表单项配置可以提前写好,再通过模块暴露出去,比如查询、删除、修改按钮。还有对于像size这类可能需要整个表单统一的属性,可以通过依赖注入的方式全局配置。
有用的渲染函数
在有了配置后,下一步就是转换成真实的表单组件。
对于按钮组部分来说,直接 v-for
一把梭就好了。同时给按钮绑定点击事件,触发自定义事件并传递出按钮的标识符:name。
{{ button.text }}
对于输入框项的部分,会有一些不同的情况,比如el-input
、el-select
等等... 我最开始的写法也很简单,直接用 v-if
区分:
这种写法虽然简单粗暴,但是注意观察 会产生发现产生了大量的冗余代码,除了一些类型的输入框独有的特殊属性外,大多都是通用属性。按照这种写法,我们有多少输入框类型,这些重复的代码就要写多少次,这样是不好的。后来阅读了一些博客文章后受到启发,于是使用Vue的渲染函数进行改造。
这里就不多介绍渲染函数的相关概念了,我们来看渲染函数的用法,它需要传入三个参数:
createElement(
'div', // {String | Object | Function},一个 HTML 标签名、组件选项对象,或者...
{}, // 一个与模板中 attribute 对应的数据对象
[] // {String | Array},子级虚拟节点 (VNodes),由 `createElement()` 构建而成
)
参照渲染函数的语法,对比各个类型的输入框,我们发现需要动态配置的异同属性都可以写在第二个参数中。那么接下来开始改造:
const _this = this
return createElement(
'div',
{},
[
createElement('label', _this.inputItem.label),
// 动态渲染
createElement(`el-${_this.inputItem.type}`, {
props: {
value: _this.inputValue,// 这里我的想法是:把绑定值脱离配置,由封装组件独立维护
size: _this.size,// 可以写一个计算属性,如果有注入全局尺寸则优先使用
disabled: _this.inputItem.disabled
},
attrs: {
placeholder: _this.inputItem.placeholder
},
on: {
input(newValue) {
_this.inputValue = newValue
_this.$emit('change', _this.inputItem.name, newValue)
}
}
}, [])// 这里用同样的方法,判断是否为select再处理渲染el-option
]
)
最后,我们再写一个外层组件包含按钮组组件和输入框项组件。具体的就不详细说,大家可以进入GitHub仓库查看。我们来看一下最终的效果:
代码总结
封装表单这件事开始于上家公司的后台项目,近期逛社区的时候刚好看到相关文章,就想着再整理记录一下。当然,这里只完成了最简单的封装,更多复杂的需求包括表单项联动、表单校验,大家可以自行进一步开发。如果你觉得本文对你有帮助,还请帮忙点个赞❤,如果你有不同的见解,欢迎评论区讨论。
当然,本文的封装属于业务组件,并不是基础组件。因此不属于基础建设的范畴,甚至可能对于你的业务场景完全不符合。所以,在开发中没有绝对的最优解决方案,只有是否合适你的业务场景。