🧱 基于 Vue3 的表单构造器实现

    271

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 在这种情况下推荐使用函数式子组件的一个原因。 传递一个函数相比直接传递一个数组有两个主要的优势:

  1. 更延迟的创建: 当数组被作为子节点传递时,数组和其中的所有元素都将在函数调用时立刻被创建。而传递一个函数时,只有在 Vue 确实需要渲染这些节点时,该函数才会被调用。这意味着在某些情况下(例如某个条件渲染的分支并未被选中),函数可能永远都不会被调用。
  2. 更好的复用: 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,我觉得不是十分灵活,所以自己动手敲了敲

当然,结果满足了当下的期望 🥱

消息盒子
# 您需要首次评论以获取消息 #
# 您需要首次评论以获取消息 #

只显示最新10条未读和已读信息