Vue表单生成器设计实践
前言
在公司一直从事基于Vue框架后台应用的前端研发,而该类应用的页面有较多的通过表单交互来增删改查的操作,为了进行优雅的开发体验,也有感于项目当前的代码,遂封装一个更合适的表单生成器form-generator.vue。
稍有从业经验的人都晓得,这类生成器在基于iview
,ant-design
这样的组件库下实现不算复杂,因此这里主要是阐述表单生成器的设计思路。
当前项目的表单生成操作有以下问题:
-
实现form生成功能的组件代码僵硬,是通过
v-if
指令根据type
字段来判断要渲染哪个组件,相应的schema如下:filterList: { list: { mallName: { name: 'mallId', displayName: '商场', type: 'SELECTION', notNull: true, value: '', options: { option: [] } }, billDate: { name: 'billDate', displayName: '账单期', type: 'MONTH', notNull: false, value: '', options: { option: [] } } // ...... } }
如果有其他类型的组件要渲染,则要更改当前的form结构数据和生成器实现组件中的v-if
相关代码。
-
可以看到,当前的form结构数据,将form的schema和相应的值
value
写在同一个对象中,这样,在将form结构数据写在Vuex
类似的状态库中时,造成form结构数据和实际使用的代码组件的上下文的不统一。 -
form结构的数据的构造较复杂,有很深的嵌套,代码的可读性不友好。
思路
Vue框架有内置组件component
,链接如下。用法是:渲染一个“元组件”为动态组件。依 is
的值,来决定哪个组件被渲染。通过component
内置组件和is
特性,我们可以很优雅的实现不同类型组件的分发渲染。这样我们只需要通过一个特定的字段componentType
指明当前要渲染的组件名,然后赋值给is
。而不需要冗长的v-if
相关代码。
其次,我们将表单生成器的schema和表单最终存放的值一起存放到model中。
那么,我们的表单生成器的结构就出来了。
{
model: {
mallId: '',
billDate: ''
},
schema: {
fields: [
{
name: 'mallId',
displayName: '商场',
componentType: 'Select',
placeholder: '不限',
required: true,
trigger: 'change',
propObj: {
transfer: true,
filterable: true,
clearable: true
},
options: []
},
{
name: 'billDate',
displayName: '账单期',
componentType: 'DatePicker',
placeholder: '请选择时间',
propObj: {
transfer: true,
type: 'month',
class: 'datePicker'
}
}
]
}
}
在实际生产使用中,schema部分存放在Vuex
状态库中,而model部分则存放在具体组件的data
中。这样做的考虑在于:类似Select
组件的枚举值是从服务端获取到的,在接口请求到数据中当即将其设置到Vuex
状态库中,而具体页面中存放的model对象可能会用于页面组件其他部分代码的操作。当然,也可以将这个form数据结构一起存放到Vuex
状态库中或者直接存放到具体组件的data
中。
结构解读
整个对象存放两个属性:model和schema。
-
model对象用于存放最终和form交互到的值,form-generator.vue组件的model属性即传递该值,并且使用了
sync
修饰符进行了数据的双向绑定。 -
schema对象主要的属性是fields,该字段是一个数组,用来存放具体待渲染为form组件的数据结构。
-
属性clearVerifyMsg用来驱动表单进行全部组件DOM层次的重置。
-
属性colProp用于每个表单组件外层包裹的
ICol
组件的栅格宽度控制,默认值为{ lg: 8, md: 12 }
,及其其他ICol
组件相关属性。 -
fields数组下的每一项的属性如下:
-
name:其值对应model中的字段名,在渲染的时候将model中的值双向绑定到当前渲染的组件中。
-
componentType:
is
属性来使用,用来确定当前要渲染的组件名 -
displayName:表单当前组件的label,会赋值给包裹着组件的
FormItem
外层组件的label
属性中。 -
hide:使用
v-if
指令控制当前组件是否渲染 -
placeholder:占位符
-
required:用在
FormItem
组件中,控制当前组件是否需要验证 -
propObj:使用
v-bind="propObj"
语法来设置当前组件的props -
options:当前待渲染组件为
Select
时,该组件的子组件Option
要渲染的列表 -
optPropObj:当前待渲染组件为
Select
时,子组件Option
的属性 -
append:当前待渲染组件为
Input
时,对应slot
的值 -
handler:在组件触发事件时,对当前组件值的特定的处理
-
trigger:控制当前组件在以什么方式触发值的校验操作,默认为blur
-
validator:使用了组件库对应的验证功能,当值为
string
时,为该类型,当值为object
时,直接使用该值进行验证,当值为function
时,为自定义的验证器
-
-
其他
该组件基于iview
组件进行开发,会用到相应的组件和表单验证功能。所以,该组件也可用于ant-design-vue
类似的组件库。
缺点
- 由于使用了
component
内置组件进行相应组件的渲染分发,所以,对组件的属性定制不友好,每个item
都需要传入需要的props
,一种解决方案是:对该组件做一次包裹,然后componentType
属性传入包裹后的组件名。 - 每个
item
的on-change
事件的回调函数的入参个数不一,且还需传入原生的event
对象,如何处理不同组件传递参数的问题还未很好处理。
具体代码
<template>
<IForm
slot="from"
ref="formValidate"
:model="model"
:rules="ruleValidate"
:label-width="labelWidth"
:inline="inline"
>
<template v-for="(item, idx) in fieldList">
<ICol v-if="!item.hide" :key="idx" v-bind="colProp">
<FormItem
:label="item.displayName"
:prop="item.name"
:required="item.required"
>
<component
:is="item.componentType"
:ref="item.name"
v-model.trim="model[item.name]"
:placeholder="item.placeholder"
v-bind="item.propObj"
@on-change="(value, $event) => handleChange(value, $event, item)"
>
<template v-if="item.options">
<Option
v-for="(opt, i) in item.options"
:key="i"
:value="opt.value"
:disabled="opt.disabled"
v-bind="item.optPropObj"
>{{ opt.label }}</Option
>
</template>
<template v-if="item.append">
<span slot="append" :style="item.appendStyle">{{
item.append
}}</span>
</template>
</component>
</FormItem>
</ICol>
</template>
<slot name="bottom">
<div class="bottomBar" :style="bottomBarStyle">
<Button type="primary" @click="handleSubmit('formValidate')">{{
okText
}}</Button>
<Button
v-if="hasReset"
class="custom_btn"
@click="handleReset('formValidate')"
>重置</Button
>
<slot name="bottom-extra-btns" />
</div>
</slot>
</IForm>
</template>
<script>
const isObject = obj =>
Object.prototype.toString.call(obj).slice(8, -1) === 'Object'
export default {
name: 'FormGenerator',
props: {
model: {
type: Object,
default() {
return {}
}
},
schema: {
type: Object,
default() {
return {
fields: []
}
},
validator: function(obj) {
return obj.hasOwnProperty('fields') && Array.isArray(obj.fields)
}
},
inline: {
type: Boolean,
default: true
},
labelWidth: {
type: [Number, String],
default: 110
},
okText: {
type: String,
default: '查询'
},
hasReset: {
type: Boolean,
default: true
},
bottomBarStyle: {
type: Object,
default() {
return {}
}
},
resetToDefaultValue: {
type: [Boolean, Object],
default: false
}
},
data() {
return {
ruleValidate: {}
}
},
computed: {
fieldList() {
return this.schema.fields
},
colProp() {
const obj = isObject(this.schema.colProp) ? this.schema.colProp : {}
return {
lg: 8,
md: 12,
...obj
}
}
},
watch: {
'schema.clearVerifyMsg': function(newVal) {
if (newVal) {
this.$refs['formValidate'].resetFields()
}
}
},
created() {
this.ruleInit()
},
methods: {
handleSubmit(name) {
this.$refs[name].validate(valid => {
if (valid) {
const result = JSON.parse(JSON.stringify(this.model))
this.$emit('on-submit', result)
}
})
},
handleReset(name) {
if (this.resetToDefaultValue) {
const newValue = isObject(this.resetToDefaultValue)
? this.resetToDefaultValue
: this.model
this.$emit('update:model', newValue)
} else {
this.$refs[name].resetFields()
}
this.$emit('on-reset')
},
handleChange(value, evt, item) {
const { handler } = item
let verifiedValue = ''
if (typeof handler === 'function') {
verifiedValue = handler(value, item, evt)
} else if (value instanceof InputEvent) {
verifiedValue = value.target.value
} else {
verifiedValue = value
}
if (verifiedValue !== undefined) {
this.$nextTick(() => {
this.$set(this.model, item.name, verifiedValue)
this.$emit('update:model', this.model)
})
}
},
ruleInit() {
this.fieldList.forEach(
({ required, validator, name, trigger = 'blur' }) => {
if (required) {
this.$set(this.ruleValidate, name, [
{
trigger,
required: true,
message: '必填项,不能为空'
}
])
} else if (validator) {
if (typeof validator === 'string') {
this.$set(this.ruleValidate, name, [
{
trigger,
required: true,
type: validators,
message: `只能输入${validator}类型`
}
])
} else if (typeof validator === 'function') {
this.$set(this.ruleValidate, name, [{ trigger, validator }])
} else if (
isObject(validator) &&
validator.hasOwnProperty('type')
) {
this.$set(this.ruleValidate, name, [validator])
} else {
throw new Error(`字段${name}的属性type的类型错误`)
}
} else {
this.$set(this.ruleValidate, name, [])
}
}
)
}
}
}
</script>
<style lang="less" scoped>
.bottomBar {
text-align: right;
margin-left: 108px;
}
</style>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
2016-11-20 FCC参阅笔记之有趣的算法(上)