前言

Vue3曾经发了一年多了,当初也是曾经转正了,Antd和element的Vue3版本也都是能够用了。Vue3刚公布没多久的时候我就上车了,过后在Github找了一圈Vue3的Admin架子,最初发现了vue-vben-admin这个我的项目,感觉这个作者写得很好,代码标准,封装啥的都很欠缺,过后Vben还只有1k多star,当初曾经10.3k了,尽管很多感觉它太臃肿了,但事实证明它的确是很好,预计前面还会缓缓减少。过后就想做一个Vben的element移植版,奈何过于懈怠,只搭起了架子,没有做后续,加上Vben的确简单,它的自定义组件不是太好移植。搁置到了往年,最近捡起来,移植了Form组件包含useForm的用法。

我的项目

我的这个element-puls版的我的项目地址vele-admin,目前移植了Model,Dialog,Form组件,用过Vben的应该晓得,就是应用useForm的模式,template模版外面的组件参数能够少传一些。

OK,开整,先说一下,Vben外面的Form组件写得比较复杂,各种util函数封装的比拟多,我这里写的时候进行了很多缩减,代码简化了很多,也更容易看懂。

剖析

useForm

Vben的很多组件都是对Antd进行二次封装,应用useFunc的模式对数据进行解决,让咱们在模版中不须要写过多的参数,也不必写大量反复的Antd组件。

上代码,useForm承受props参数,props就是Form组件的属性,Vben外面加了更多本人的属性,领有更多自定义的性能,我这里就不做那么多了,我的props类型基本上就是element-plus的form属性,除了schemas,基本上都是间接传给el-form的,schemas是为了去主动增加Form的内容组件的,前面再具体说。

  • schemas 表单配置属性
  • model 表单数据,须要传入ref或reactive的响应式变量
  • rules 表单验证规定
export interface FormProps {  schemas?: FormSchema[];  // 表单数据对象  model?: Recordable;  // 表单验证规定  rules: any;  //     行内表单模式  inline: boolean;  // 表单域标签的地位, 如果值为 left 或者 right 时,则须要设置 label-width  labelPosition: string;  // 表单域标签的宽度,例如 '50px'。 作为 Form 间接子元素的 form-item 会继承该值。 反对 auto。  labelWidth: string | number;  // 表单域标签的后缀  labelSuffix: string;  // 是否显示必填字段的标签旁边的红色星号  hideRequiredAsterisk: boolean;  // 是否显示校验错误信息  showMessage: boolean;  // 是否以行内模式展现校验信息  inlineMessage: boolean;  // 是否在输入框中显示校验后果反馈图标  statusIcon: boolean;  // 是否在 rules 属性扭转后立刻触发一次验证  validateOnRuleChange: boolean;  // 用于管制该表单内组件的尺寸    strin  size: string;  // 是否禁用该表单内的所有组件。 若设置为 true,则表单内组件上的 disabled 属性不再失效  disabled: boolean;}

useForm函数次要的性能就是返回一个register和一些Form操作方法,这些办法与element-plus官网办法名统一,实际上也是间接去调用el-form的原生办法,Vben也是间接去调用antd的办法。当然,它有一些自定义操作表单的办法。

  • setProps 动静设置表单属性
  • validate 对整个表单作验证
  • resetFields 对整个表单进行重置,将所有字段值重置为初始值并移除校验后果
  • clearValidate 清理指定字段的表单验证信息
  • validateField 对局部表单字段进行校验的办法
  • scrollToField 滚动到指定表单字段

Vben外面这里返回的办法会更多,我这里没有全副做完,因为我的数据处理和Vben不太一样,有几个办法也不须要。比方getFieldsValue获取表单某个属性值和setFieldsValue设置表单属性值,因为我是间接去应用里面申明的响应式对象,所以里面间接应用/设置formData就能够了,这也和element-plus和antd不同有关系,前面再细说。

useForm其实没有什么特地的,返回的register函数须要在应用时传给VeForm,外部会把VeForm的组件实例传给register,而后在useForm内就能够应用组件实例去调用VeForm外部的办法。

其实精确说来,我感觉useForm拿到的instance也不算是VeForm的组件实例,只是VeForm给useForm提供的一个蕴含外部办法属性的对象,因而我把这个变量改成了formAction,而不是Vben外面的formRef,不过这个倒是无所谓了,无伤大雅。

import { ref, onUnmounted, unref, watch, nextTick } from "vue";import { FormActionType, FormProps } from "../types";import { throwError } from "/@/utils/common/log";import { isProdMode } from "/@/utils/env/env";export default function useForm(props?: Partial<FormProps>) {  const formAction = ref<Nullable<FormActionType>>(null);  const loadedRef = ref<Nullable<boolean>>(false);  function register(instance: FormActionType) {    if (isProdMode()) {      // 开发环境下,组件卸载后开释内存      onUnmounted(() => {        formAction.value = null;        loadedRef.value = null;      });    }    // form 组件实例 instance 已存在    // 实际上 register 拿到的并不是 组件实例, 只是挂载了一些组件外部办法的 对象 formAction    if (unref(loadedRef) && isProdMode() && instance === unref(formAction)) {      return;    }    formAction.value = instance;    loadedRef.value = true;    // 监听 props, 若props扭转了    // 则应用 form 实例调用外部的 setProps 办法将新的props设置到form组件外部    watch(      () => props,      () => {        if (props) {          instance.setProps(props);        }      },      { immediate: true, deep: true }    );  }  async function getForm() {    const form = unref(formAction);    if (!form) {      throwError(        "The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!"      );    }    await nextTick();    return form as FormActionType;  }  const methods: FormActionType = {    async setProps(formProps: Partial<FormProps>) {      const form = await getForm();      form.setProps(formProps);    },    async validate(callback: (valid: any) => void) {      const form = await getForm();      form.validate(callback);    },    async validateField(      props: string | string[],      callback: (err: string) => void    ) {      const form = await getForm();      form.validateField(props, callback);    },    async resetFields() {      const form = await getForm();      form.resetFields();    },    async clearValidate() {      const form = await getForm();      form.clearValidate();    },    async scrollToField(prop: string) {      const form = await getForm();      form.scrollToField(prop);    },  };  return { register, methods };}

useFormEvents

这个hook函数提供resetFieldsclearValidatevalidatevalidateFieldscrollToField表单操作方法。

export interface FormActionType {  // 设置表单属性  setProps: (props: Partial<FormProps>) => void;  // 对整个表单作验证  validate: (callback: (valid: any) => void) => void;  // 对整个表单进行重置,将所有字段值重置为初始值并移除校验后果  resetFields: () => void;  // 清理指定字段的表单验证信息  clearValidate: (props?: string | string[]) => void;  // 对局部表单字段进行校验的办法  validateField: (    props: string | string[],    callback: (err: string) => void  ) => void;  // 滚动到指定表单字段  scrollToField: (prop: string) => void;}

实际上只是在VeForm里调用的时候将el-form的实例formElRef间接传进来,而后间接去调用el-from提供的对应api。 Vben因为办法比拟多所以抽出来了,其实间接写在VeForm外面也没关系。

我这里不同的resetFields办法和Vben不同,Vben是手动去对贮存了表单数据的变量formModel进行批改的,这里因为el-form的resetFields能够间接重置到初始值,我就把这段删掉了。

革除测验信息和对数据进行测验这两个办法就间接调el-form的api,测验数据这里有一个大坑,当然如果留神到的人就没事,我过后没留神prop这个属性,导致始终无奈触法测验,给我找了良久,最初调试的时候才发现prop是undefined,气死我了。

import { nextTick, Ref, unref } from "vue";import type { FormActionType, FormProps } from "../types";export interface UseFormActionContext {  propsRef: Ref<Partial<FormProps>>;  formElRef: Ref<FormActionType>;}export function useFormEvents({ formElRef }: UseFormActionContext) {  async function resetFields() {    await unref(formElRef).resetFields();    nextTick(() => clearValidate());  }  async function clearValidate(name?: string | string[]) {    await unref(formElRef).clearValidate(name);  }  async function validate(callback: (valid: any) => void) {    return await unref(formElRef).validate(callback);  }  async function validateField(    prop: string | string[],    callback: (err: string) => void  ) {    return await unref(formElRef).validateField(prop, callback);  }  async function scrollToField(prop: string) {    return await unref(formElRef).scrollToField(prop);  }  return { resetFields, clearValidate, validate, validateField, scrollToField };}

VeForm

接下来就是最重要的组件VeForm了,获取el-form组件实例,再将这些表单操作方法对象在onMounted组件挂载之后通过里面应用VeForm订阅的register事件传递到useForm中,useForm再将formAction对象的办法提供给内部组件。

  • getBindValue 收集内部传入的所有参数,包含接管的,没接管的,useForm里传的,VeForm组件上间接传的,收集起来统统传给el-form
  • getSchema 表单配置对象
  • formRef el-form组件实例,传给useFormEvents去调用el-form的提供办法,Vben里还传给了useFormValues去做表单数据的解决和其余hook函数,我这里没有做
  • setFormModel 给VeFormItem提供的设置表单数据办法
  • setProps 给内部提供动静设置表单属性的办法
  • formAction 给内部提供的表单操作对象
<script lang="ts" setup>import { computed, onMounted, ref, unref, useAttrs } from "vue";import type { Ref } from "vue";import type { FormActionType, FormProps } from "./types";import VeFormItem from "./components/VeFormItem.vue";import { useFormEvents } from "./hooks/useFormEvents";import { useFormValues } from "./hooks/useFormValues";const attrs = useAttrs();const emit = defineEmits(["register"]);const props = defineProps();const propsRef = ref<Partial<FormProps>>({});const formRef = ref<Nullable<FormActionType>>(null);const defaultValueRef: Recordable = {};// 合并接管的所有参数const getBindValue = computed<Recordable>(() => ({  ...attrs,  ...props,  ...propsRef.value,}));const getSchema = computed(() => {  const { schemas } = unref(propsRef);  return schemas || [];});const { validate, resetFields, clearValidate } = useFormEvents({  propsRef,  formElRef: formRef as Ref<FormActionType>,  defaultValueRef,});const { initDefault } = useFormValues({  defaultValueRef,  getSchema,  propsRef,});function setFormModel(key: string, value: any) {  if (propsRef.value.model) {    propsRef.value.model[key] = value;  }}function setProps(formProps: Partial<FormProps>) {  propsRef.value = { ...propsRef.value, ...formProps };}const formAction: Partial<FormActionType> = {  setProps,  validate,  resetFields,  clearValidate,};// 裸露给里面的组件实例应用defineExpose(formAction);onMounted(() => {  emit("register", formAction);  initDefault();});</script><template>  <el-form ref="formRef" v-bind="getBindValue">    <slot name="formHeader"></slot>    <template v-for="schema in getSchema" :key="schema.field">      <VeFormItem        :schema="schema"        :formProps="propsRef"        :setFormModel="setFormModel"      >        <template #[item]="data" v-for="item in Object.keys($slots)">          <slot :name="item" v-bind="data || {}"></slot>        </template>      </VeFormItem>    </template>    <slot name="formFooter"></slot>  </el-form></template>

VeFormItem

VeForm中通过表单配置对象getSchema循环应用VeFormItem,将propsRefsetFormModel以及对应的schema传入VeFormItem中。Vben的FormItem是用jsx模式写的,我这里简化比拟多,感觉没有必要用,就间接模版写了。

  • schema

除了没有特地大必要用jsx之外,还有一个起因就是我jsx不怎么用,再写label插槽的时候用jsx我不晓得怎么写,就用模版了。如果schema的label属性VNode,就插刀ElFormItem的label插槽中;不是的话就传给ElFormItem。

  • renderComponent

renderComponent函数返回schema配置的组件,componentMap是一个事后写好的Map,设置了表单罕用组件,当用户配置了render时,就间接应用render,render必须是一个VNode,没有则通过component属性去componentMap上取。

  • getModelValue

getModelValue是动静设置的双向绑定数据,它是一个可设置的计算属性,读取时返回由VeForm传入的formProps上的model上对应的属性,设置的时候去调用VeForm传入的setFormModel办法,相当于间接去操作到了用户在应用useForm时传入的model对象,因而model对象必须是reactive/ref申明的可批改的双向绑定数据。

  • formModel(formData)

数据这里的做法和Vben不是太一样,Vben是在外部申明的formModel对象,通过schema去配置defaultValue,给内部提供setFieldsValue和getFieldsValue的办法。也是因为element-plus的form是间接应用v-model去绑定input等其余理论的内容组件的,并且我也心愿在里面本人申明formData,而后formData也能实时更新,间接去应用formData给接口应用、传给子组件或是其余操作。因而所有表单数据对象从里到内始终用的是一个对象,就是里面申明并传入useForm的model属性的变量formData。

  • compAttr 将schema配置对象的componentProps属性传给理论的内容组件。
export interface FormSchema {  // 字段属性名  field: string;  // 标签上显示的自定义内容  label: string | VNode;  component: ComponentType;  // 子组件 属性  componentProps?: object;  // 子组件  render?: VNode;}
<script lang="ts" setup>import { computed, ref, useAttrs } from "vue";import { componentMap } from "../componentMap";import { FormSchema } from "../types";import { ElFormItem } from "element-plus";import { isString } from "/@/utils/help/is";const attrs = useAttrs();const props = defineProps<{  schema: FormSchema;  formProps: Recordable;  setFormModel: (k: string, v: any) => {};}>();const { component, field, label } = props.schema;const labelIsVNode = computed(() => !isString(label));const compAttr = computed(() => ({  ...props.schema.componentProps,}));// 内容组件的双向绑定数据const getModelValue = computed({  get() {    return props.formProps.model[field];  },  set(value) {    props.setFormModel(field, value);  },});const getBindValue = computed(() => {  const value: Recordable = {    ...attrs,    prop: field,  };  if (isString(label)) {    value.label = label;  }  return value;});function renderComponent() {  if (props.schema.render) {    return props.schema.render;  }  return componentMap.get(component);}</script><template>  <ElFormItem v-bind="getBindValue">    <template v-if="labelIsVNode" #label>      <component :is="label" />    </template>    <component      v-model="getModelValue"      v-bind="compAttr"      :is="renderComponent()"    />  </ElFormItem></template>

应用

申明表单配置对象schemas,表单初始数据formData,表单验证规定rules,传入useForm;将register给VeForm绑定上即可。

import type { FormSchema } from "/@/components/VeForm/types";import { reactive } from "vue";const schemas: FormSchema[] = [  {    field: "name",    label: "姓名",    component: "Input",  },  {    field: "age",    label: "年纪",    component: "InputNumber",  }]const rules = reactive({  name: [    {      required: true,      message: "Please input name",      trigger: "blur",    },    {      min: 3,      max: 5,      message: "Length should be 3 to 5",      trigger: "blur",    },  ],  age: [    {      required: true,      message: "Please input age",      trigger: "blur",    },  ]})

如果把schemas和rules这种一单申明之后不会太做批改的数据抽出去,那组件就变得更简洁了,对于我这种喜爱极简的人来说,这组件看着太难受了。

import VeForm, { useForm } from "/@/components/VeForm";import { ref } from "vue";const formData = reactive({  name: "shellingfordly",  age: 24,});const { register, methods } = useForm({  model: formData,  rules,  schemas,});const submitForm = () => {  methods.validate((valid: any) => {    if (valid) {      console.log("submit!", valid);    } else {      console.log("error submit!", valid);      return false;    }  });};const resetForm = () => {  methods.resetFields()};const clearValidate = () => {  methods.clearValidate()};<template>  <VeForm @register="register" />  <ElButton @click="submitForm">submitForm</ElButton></template>

Vben中能够动静设置schema,我这里没有做。当然还有一些它自定义的办法我也没有写,我前两天移植的时候在表单校验那里卡住了,就因为过后没留神到prop这个属性,始终没有发现,找个半天的bug,最初调试的时候才发现prop是undefined,比照了一下间接应用ElForm,外面是有值的,这才看到文档外面写着在应用 validate、resetFields 办法的状况下,该属性是必填的,这个故事通知咱们要好好看文档啊,文档里写得清清楚楚。这个bug给我整吐了,导致前面我不想写了哈哈哈哈,就没有加动静增加schema,以及提交表单数据的button。不过没关系,总体来说性能是实现了。有趣味的小伙伴能够去帮我增加哈哈哈哈,当然也能够移植其余组件。我的项目地址vele-admin。