🧱 基于 Vue3 的表单构造器实现
0️⃣ Before
昨天准备继续后台的 Vue3 重构,于是新建页面开始Coding...
<template>
<div class="p-[20px]">
// TODO
</div>
</template>
常规的页面大多由 QueryForm
PageTable
DialogForm
三个部分组成
我并不想写过多重复的代码,所以考虑基于 ElementPlus
进行封装
1️⃣ Code
QueryForm
├── Fields
│ ├── useSelectorField.ts
│ └── useTextField.ts
├── index.ts
└── index.vue
QueryForm/index.ts
import { h, VNode } from 'vue'
import QueuryForm from '@/core/QueryForm/index.vue'
export type FormDefine = {
modelValue?: any
rules?: any
disabled?: boolean
labelWidth?: number
formItems?: FormItem[]
className?: string
actions?: Actions[]
}
export type FormItem = {
type?: VNode
key?: string
label?: string
}
export type Actions = {
type: 'default' | 'primary' | 'success' | 'warning' | 'info' | 'danger'
text: string
handler: () => void
}
export default (formDefines: FormDefine) => {
return h(QueuryForm, formDefines)
}
这里是组件的入口,定义了 FormDefine
, 需要返回一个 VNode
,它会被加入至页面中
QueryForm/index.vue
<template>
<div :class="[`flex`, className]">
<el-form
:inline="true"
:label-width="labelWidth"
ref="form"
:model="modelValue.value"
:rules="rules"
:disabled="disabled"
>
<template v-for="(item, index) in formItems" :key="index">
<el-form-item :label="item.label">
<component :is="item.type" v-model="modelValue.value[item.key!]" />
</el-form-item>
</template>
<el-form-item>
<template v-for="(item, index) in actions" :key="index">
<el-button :type="item.type" @click="item.handler">{{ item.text }}</el-button>
</template>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup name="QueryForm">
import { FormDefine } from '@/core/QueryForm'
const props = withDefaults(defineProps<FormDefine>(), {
modelValue: {},
rules: {},
disabled: false,
labelWidth: 100,
formItems: () => [],
className: '',
actions: () => [],
})
</script>
QueryForm/Fields
用来存放一些类型实现,也需要返回一个VNode,如 useTextField
, useDatePickerField
, useSelectorField
...
eg. useTextField
import { h } from 'vue'
import { ElInput } from 'element-plus'
export default () => {
return h(ElInput)
}
eg. useSelectorField
import { h, VNode } from 'vue'
import { ElSelect, ElOption } from 'element-plus'
type SelectDefine = {
options: any
label?: string
value?: string
}
export default (define: SelectDefine) => {
const options: (() => VNode[]) = () => {
return define.options.map((item: any) => {
return h(ElOption, { label: item[define.label || 'label'], value: item[define.value || 'value'] })
})
}
return h(ElSelect, options)
}
这里 options
之前是直接返回的 VNode[]
,后续改成了函数,有提示警告 ⚠️,和 Vue 优化策略有关
在你的
useSelectorField
函数中,你简单地将ElSelect
的子节点(ElOption
)作为数组直接传递给ElSelect
。而在 Vue 3 中,它希望你将这些子节点包装在一个函数中,这样 Vue 3 就可以更有效地对其进行更新和渲染。 你需要将子节点定义为一个返回类组件数组的函数,如下所示:export default (define: SelectDefine) => { const optionsFunction: (() => VNode[]) = () => { return define.options.map((item: any) => { return h(ElOption, { label: item[define.label || 'label'], value: item[define.value || 'value'] }) }); }; //将options的数组包装在函数里 return h(ElSelect, {}, optionsFunction); //将这个函数作为children传递给 `h`函数 }
使用这种方式, Vue 3 可以按需渲染和更新子节点,从而优化性能。 注意到,
h
函数的第三个参数应该是一个返回VNode[]
类型的函数,或者一个对象,这个对象的每个键都映射到一个返回VNode[]
的函数。每个函数都表示一个具名插槽。默认插槽可以作为这个对象的default
属性。🙏,警告处理掉了,比较好奇这里为什么要这样处理,官方文档中对于
h
函数部分文档给出的示例,形如这样h('div', [h('span'), h('span')])
,是一个数组,而在我上述的代码中,返回值实际上也是一个有h
组成的数组Vue 的渲染函数
h
函数在正常使用中接受数组作为其子组件参数是没有问题的,这是 Vue 的标准用法。在很多情况下,你仍然可以使用类似h('div', [h('span'), h('span')])
的代码,且这有可能并没有任何问题。 然而在你的特定场景中,嵌套了useSelectorField
,实际上是在运行时动态地创建了数组。这种创建策略在 Vue3 的优化策略中可能会导致性能损失。这是为何 Vue 3 在这种情况下推荐使用函数式子组件的一个原因。 传递一个函数相比直接传递一个数组有两个主要的优势:
- 更延迟的创建: 当数组被作为子节点传递时,数组和其中的所有元素都将在函数调用时立刻被创建。而传递一个函数时,只有在 Vue 确实需要渲染这些节点时,该函数才会被调用。这意味着在某些情况下(例如某个条件渲染的分支并未被选中),函数可能永远都不会被调用。
- 更好的复用: Vue3 的优化策略之一是在渲染更新时尽可能地复用旧的节点。当子节点是由函数创建时,使得 Vue 可以在重新渲染组件时仍然使用旧的节点,以此避免创建新的元素。
那我能这样理解吗?对于比较简单的,可以使用
h('div', [h('span'), h('span')])
进行一次性的创建,逻辑更加复杂的则需要返回以函数的形式是的,你的理解是正确的。对于简单的、一次性创建的子节点,直接使用
h('div', [h('span'), h('span')])
是完全没有问题的。 但是,如果你知道子节点可能会频繁改变,或者它们是在运行时根据某些复杂逻辑动态创建的,那么使用函数作为h
函数的子节点参数就是更好的选择。这使 Vue 能够只在必要的时候执行渲染逻辑,并且可以更有效地复用已存在的节点。 对于大多数的用例,性能影响是很小的,但 Vue 仍然提供了函数式子节点作为一个优化手段,它可以在特定的场景下提供性能提升,尤其是对于那些需要生成大量动态子节点的场景。
总结下来就是对于更加复杂的情况,h
函数的第三个参数应为 返回 VNode[]
的函数
判断依据大概就是除了原始 HTML标签
外,均可以使用函数来作为参数
使用
// script
<script lang="ts" setup>
import QueryForm from '@/core/QueryForm'
const QueryFormNode = QueryForm({
modelValue: queryFrom,
labelWidth: 60,
formItems: [
{
label: '姓名',
key: 'name',
type: useTextField(),
},
{
label: '类型',
key: 'type',
type: useSelectorField({
options: [
{ label: 'Type1', value: '1' },
{ label: 'Type2', value: '2' },
{ label: 'Type3', value: '3' },
],
}),
},
],
actions: [
{
type: 'primary',
text: '查询',
handler: () => {
console.log(queryFrom.value)
},
},
{
type: 'default',
text: '重置',
handler: () => {
queryFrom.value = {}
},
},
],
})
</script>
// template
<template>
<QueryFormNode></QueryFormNode>
</template>
2️⃣ End
通篇下来有种记流水账的感觉,可能是因为这里也并没有什么难点,没啥好写的
折腾的缘由一方面和上述所说的一致,不想写重复的代码,另一方面是兼职的前端框架,那边的大佬也同样是基于 ElementPlus
,我觉得不是十分灵活,所以自己动手敲了敲
当然,结果满足了当下的期望 🥱
空空如也!