前端表单开发最佳实践

568 阅读4分钟

如何写出高质量的前端代码》学习笔记

表单开发是前端开发中不可避免的一个重要话题,尤其是在管理系统中。表单开发不仅占用大量时间,还容易出现可读性、可维护性差的问题。我们来探讨表单开发中的常见问题及解决方案。

表单组件的定义

表单组件是指用于处理表单数据的可复用组件。开发表单组件时,应遵循一定的开发范式,主要分为受控组件和非受控组件。

受控组件

受控组件的内部状态由外部属性控制,必须满足以下条件:

  • 存在一个名为value的属性,初始值由value决定,value变化后组件状态也随之变化。
  • 组件对外抛出onChange事件,内部状态变化后通过事件将最新值传递给父组件。

非受控组件

非受控组件的内部状态由组件自身维护,满足以下条件:

  • 提供一个defaultValue属性,初始状态由其决定,后续状态随用户交互变化。
  • 提供一个方法获取内部状态,如getValue()

表单开发常见问题及解决方案

在表单开发中,常常会遇到以下几个问题。下面详细说明这些问题及其解决方案。

问题1:充满细节

问题描述:表单开发中,表单项通常包含大量的业务逻辑和细节。如果所有逻辑都集中在一个文件中,会导致代码难以维护和阅读。

解决方案

  • 提取表单项为独立组件: 将每个表单项提取为独立的组件。这样可以提高代码的可读性和可维护性。
  • 高内聚: 每个表单项组件应包含其完整的逻辑和样式,避免逻辑分散。
  • 复用性: 提取的表单项组件可以在其他表单中复用,减少重复开发。

示例:

    <template>
      <el-form :model="formData">
        <form-item label="商品名称" prop="name">
          <el-input v-model="formData.name"/>
        </form-item>
        <form-item label="商品图片" prop="pics">
          <ProductPicsFormItem v-model="formData.pics"/>
        </form-item>
        <!-- 其他表单项 -->
      </el-form>
    </template>

问题2:繁琐的DOM

问题描述: 表单项配置繁琐,导致大量的DOM代码,难以管理和阅读。

解决方案:

  • 配置驱动表单生成: 使用配置对象来描述表单结构,通过数据驱动表单的生成。
  • 高内聚配置: 将表单项的配置和校验规则放在一起。

示例:

    <template>
      <config-form :model="formData" :fields="fields"/>
    </template>
    <script>
    export default {
      data() {
        return {
          formData: {},
          fields: {
            name: {
              label: '名称',
              component: 'input',
              componentProps: {
                placeholder: '请填写名称',
                minlength: 2,
                maxlength: 30,
                clearable: true
              },
              rules: [{required: true}]
            },
            // 其他字段配置
          }
        }
      }
    }
    </script>

问题3:表单项组件封装不规范

问题描述: 表单项组件封装不规范,导致耦合性高和可读性差。

解决方案:

  • 遵循组件开发规范: 受控组件应提供value属性和onChange事件,非受控组件应提供defaultValue属性和getValue方法。
  • 避免耦合: 不要在组件内部直接修改传入的props,而是通过事件通知父组件。

示例:

    <template>
      <el-form>
        <el-form-item name="category">
          <CategoryFormItem :value="formData.category" @change="updateCategory"/>
        </el-form-item>
      </el-form>
    </template>
    <script>
    export default {
      methods: {
        updateCategory(newValue) {
          this.formData.category = newValue;
        }
      }
    }
    </script>

问题4:校验规则不统一

问题描述: 不同表单中相同业务的校验规则不一致,导致维护困难。

解决方案:

  • 提取常用校验规则到常量中: 将相同业务的校验规则提取到一个常量中,确保一致性。
  • 业务分离: 不同业务即使校验规则相同,也应分开定义,避免耦合。

示例:

    // /const/validate.js
    export const UserNameValidate = [
      {
        pattern: /^[a-zA-Z]\w{0,29}$/, 
        message: '用户名必须以字母开头,只能包含字母数组和下划线,不超过30个字符'
      }
    ]

复杂业务表单的开发

思路

  1. 拆分表单: 将复杂表单拆分为多个子表单,每个子表单负责一部分逻辑。
  2. 数据流管理: 使用受控或非受控组件的方式管理子表单的数据流。
  3. 表单校验: 每个子表单提供一个validate方法,主表单在提交时调用这些方法进行校验。

非受控组件方式

在非受控组件方式中,子表单通过defaultValue属性初始化数据,后续数据由子表单自行维护。

主表单实现

    <template>
      <div>
        <BaseForm ref="baseForm" :defaultValue="formData.baseInfo"/>
        <DetailForm ref="detailForm" :defaultValue="formData.detail"/>
        <el-button @click="submit">提交</el-button>
      </div>
    </template>

    <script>
    import BaseForm from "@/components/big-form/BaseForm";
    import DetailForm from "@/components/big-form/DetailForm";

    export default {
      data() {
        return {
          formData: {
            baseInfo: { name: '' },
            detail: { description: '' }
          }
        };
      },
      methods: {
        submit() {
          Promise.all([
            this.$refs.baseForm.validate(),
            this.$refs.detailForm.validate()
          ]).then(([baseFormData, detailFormData]) => {
            console.log(baseFormData, detailFormData);
          }).catch(error => {
            console.log(error);
          });
        }
      }
    };
    </script>

子表单实现(BaseForm)

    <template>
      <div>
        <div>基础信息</div>
        <el-form ref="form" :model="formData" :rules="rules">
          <el-form-item prop="name" label="姓名">
            <el-input v-model="formData.name" />
          </el-form-item>
        </el-form>
      </div>
    </template>

    <script>
    export default {
      props: {
        defaultValue: {
          type: Object,
          default: () => ({})
        }
      },
      data() {
        return {
          formData: { ...this.defaultValue },
          rules: {
            name: [{ required: true, message: '姓名不能为空' }]
          }
        };
      },
      methods: {
        validate() {
          return this.$refs.form.validate().then(() => {
            return this.formData;
          });
        }
      }
    };
    </script>

受控组件方式

在受控组件方式中,子表单支持v-model双向绑定,主表单可以实时获取子表单的数据。

主表单实现

    <template>
      <div>
        <BaseForm ref="baseForm" v-model="formData"/>
        <DetailForm ref="detailForm" v-model="formData"/>
        <el-button @click="submit">提交</el-button>
      </div>
    </template>

    <script>
    import BaseForm from "@/components/big-form/BaseForm";
    import DetailForm from "@/components/big-form/DetailForm";

    export default {
      data() {
        return {
          formData: {
            name: '',
            description: ''
          }
        };
      },
      methods: {
        submit() {
          Promise.all([
            this.$refs.baseForm.validate(),
            this.$refs.detailForm.validate()
          ]).then(() => {
            console.log('this.formData', this.formData);
          }).catch(error => {
            console.log(error);
          });
        }
      }
    };
    </script>

子表单实现(BaseForm)

    <template>
      <div>
        <div>基础信息</div>
        <el-form ref="form" :model="formData" :rules="rules">
          <el-form-item prop="name" label="姓名">
            <el-input v-model="formData.name" />
          </el-form-item>
        </el-form>
      </div>
    </template>

    <script>
    export default {
      model: {
        event: 'change'
      },
      props: {
        value: {
          type: Object,
          default: () => ({})
        }
      },
      computed: {
        formData: {
          get() {
            return this.value;
          },
          set(val) {
            this.$emit('change', val);
          }
        }
      },
      data() {
        return {
          rules: {
            name: [{ required: true, message: '姓名不能为空' }]
          }
        };
      },
      methods: {
        validate() {
          return this.$refs.form.validate();
        }
      }
    };
    </script>
  • 非受控组件: 简单易实现,适合不需要实时数据同步的场景。
  • 受控组件: 实时数据同步,适合需要多个子表单交互的场景。

总结

表单组件开发应遵循受控和非受控组件的范式:

  • 受控组件:支持value属性和onChange事件。
  • 非受控组件:支持defaultValue属性和getValue方法。

建议优先开发受控组件,以简化使用。通过提取表单项组件、使用配置表单、统一校验规则等方式,提高表单开发的质量和效率。对于复杂表单,采用分治思想进行拆分和管理。