Vue2动态表单组件封装:两种风格对比与最佳实践

作者:雾松时间:2026-10-25 18:24:10浏览:651

本文深入探讨Vue2中动态表单组件的封装策略,对比了完全封装JSON配置与基于JSX函数组件暴露Slot两种风格。重点推荐后者,通过保留Template语法和Slot机制,在复用UI逻辑的同时避免组件臃肿,解决动态显示、联动及校验等复杂场景,并提供完整代码示例。

在封装组件时,需要关注几个核心注意点。

  • 首先,应避免为了封装而强行封装,需评估实际复用价值。
  • 其次,仅封装那些相对固定的部分,将变化部分通过Slot或其他方式暴露出来,交由调用者自行实现。
  • 为了提高组件的复用率,建议优先封装为UI组件而非业务组件。这意味着组件内部使用的数据应通过Props获取,而不是直接在组件内部发起Ajax请求或从Vuex中读取。

动态表单的核心在于动态控制,具体控制哪些内容?

常见的动态控制场景包括以下几种:

  • 不同界面显示不同的表单项组合,例如A界面显示A、B、C三个表单项,而B界面显示A、B、E、F三个表单项。
  • 根据条件动态显示或隐藏表单项,例如A界面默认显示A、B两个表单项,仅当A项的值为特定值x时,C表单项才显示。
  • 根据条件动态启用或禁用表单项,例如A界面默认显示A、B、C三个表单项,A和B默认可用,但C默认禁用;仅当A项的值为特定值x时,C表单项才变为可用状态。
  • 表单项之间的值依赖关系,例如A界面中B表单项的可选项依赖于A表单项的选择或填写结果。
  • 响应式布局调整,即根据不同的屏幕分辨率,每行可显示的表单项数量不同。

接下来探讨动态表单的具体封装方式。

在两种主要风格中,建议优先采用风格二。

风格一:完全封装JSON配置模式

该风格的特点简介如下:

它将el-form、el-form-item、el-input等组件完全封装在动态表单组件内部,通过for循环渲染表单项。外部通过传入JSON配置对象,实现字段的动态显示与隐藏控制。

这种风格的优点包括:

  • 减少了如<el-form-item><el-input></el-input></el-form-item>这类代码的重复编写。
  • 可以轻易地调整表单项的先后顺序。

然而,其缺点同样明显:

  • 如果所有用到表单的地方都使用这一个表单组件,该组件后期会变得非常庞大,充斥大量逻辑控制代码,导致后期难以维护。
  • 为了最大程度保留原始表单的配置和事件,需要使用v-bind=attrs和v-on=evts方式进行配置。这相对于Template语法来说,JSON对象的配置方式让Vue开发者感到不太习惯。
  • 通过v-bind=attrs方式设置原组件的属性时,若想设置一些默认值会变得麻烦。因为在Vue2中,设置了v-bind=attrs之后,无法在模板中直接设置默认属性,必须在封装组件内部,获取到配置对象时进行繁琐的初始值设置。
  • 如果表单model对象是通过Prop传入,那么表单内部修改这个表单对象需要做特殊处理,以规避ESLint对单项数据流的检查报错。

以下是风格一的代码实现示例。

动态表单组件的实现代码如下:

<template>
  <el-form :model="formModel" v-bind="elFormAttrs">
    <el-form-item
      v-for="(formItemConfig, index) in formItemConfigArr"
      :key="`${formItemConfig.prop}-${index}`"
      :prop="formItemConfig.prop"
      :rules="formItemConfig.rules"
    >
      <template #label>
        <div class="o-custom-label">{{ formItemConfig.label }}</div>
      </template>
      <el-input
        v-if="formItemConfig.itemType === 'input'"
        v-model="formModel[formItemConfig.prop]"
      ></el-input>
      <el-select
        v-else-if="formItemConfig.itemType === 'select'"
        v-model="formModel[formItemConfig.prop]"
      >
        <template v-if="formItemConfig.optionArr">
          <el-option
            v-for="(option, pos) in formItemConfig.optionArr"
            :key="`${option.value}-${pos}`"
            :value="option.value"
            :label="option.label"
          ></el-option>
        </template>
      </el-select>
    </el-form-item>
  </el-form>
</template>

<script>
export default {
  name: "DynForm",
  model: {
    event: "change",
    prop: "formData",
  },
  props: {
    formData: {
      type: Object,
    },
    /**
     * prop: 唯一标识
     * itemType: 表单项类型
     * rules: 表单验证规则
     * label: 显示标签
     * optionArr: 下拉值
     */
    formItemConfigArr: {
      type: Array,
      default: () => [],
    },
    // el-form支持的所有属性
    elFormAttrs: {
      type: Object,
    },
  },
  data() {
    return {
      formModel: this.formData ? this.formData : {},
    };
  },
  watch: {
    // 监听formData改变,将formData的值设置给组件内部的formModel,规避eslint对单项数据流的规则检测报错
    formData: {
      handler(newVal) {
        this.formModel = newVal;
      },
    },
  },
  methods: {},
};
</script>
<style scoped></style>

调用动态表单组件的方式如下:

<template>
  <div class="demo-form">
    <DynForm
      v-model="formData"
      :form-item-config-arr="formItemConfigArr"
      :el-form-attrs="elFormAttrs"
    ></DynForm>
  </div>
</template>

<script>
import DynForm from "@/components/form/dyn/DynForm";
export default {
  name: "FormTemplate3",
  components: {
    DynForm,
  },
  props: {
    // input文本框数量
    count: {
      type: Number,
      default: 50,
    },
  },
  data() {
    // 表单项配置
    const formItemConfigArr = [
      {
        prop: "sex",
        label: "成语故",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "name",
        label: "姓名",
        itemType: "input",
      },
      {
        prop: "three",
        label: "成语故事",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "four",
        label: "一二三四五",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "five",
        label: "一二三四五六",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "six",
        label: "一二三四五六七",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "seven",
        label: "一二三四五六七八",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "eight",
        label: "一二三四五六七八九",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "ten",
        label: "ten",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "zero",
        label: "zero",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "a",
        label: "hello",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "b",
        label: "hello world",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "c",
        label: "good idea thank",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "d",
        label: "good configuration",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "d",
        label: "good idea thank configuration",
        itemType: "select",
        optionArr: [],
      },
    ];
    return {
      elFormAttrs: {
        inline: true,
        labelWidth: "81px",
      },
      formData: null,
      formItemConfigArr,
    };
  },
  created() {
    this.loadFormData();
    this.loadSexOptions();
  },
  methods: {
    loadFormData() {
      setTimeout(() => {
        this.formData = {
          name: "张三",
          sex: 1,
        };
      }, 1000);
    },
    loadSexOptions() {
      setTimeout(() => {
        const item = this.formItemConfigArr.find((item) => item.prop === "sex");
        if (item) {
          const sexOptionArr = [
            { value: 1, label: "选项1" },
            { value: 2, label: "选项2" },
            { value: 3, label: "选项3" },
          ];
          item.optionArr = sexOptionArr;
        }
      }, 1500);
    },
  },
};
</script>

<style scoped lang="scss">
.js-validate-form ::v-deep(.is-error .o-show-data) {
  color: red;
}
.demo-form ::v-deep(.el-form-item__label) {
  line-height: 1.6;
  display: inline-flex;
  height: 40px;
  justify-items: right;
  justify-content: flex-end;
  align-items: center;
}
.demo-form ::v-deep(.el-form-item__label .o-custom-label) {
  word-break: break-word;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; /* 这里是超出几行省略 */
  overflow: hidden;
}
</style>

风格二:JSX函数组件模式(推荐优先采用此风格)

该风格的特点介绍如下:

通过函数组件,以JSX语法对el-form-item进行封装,仅封装模板代码部分,动态部分全部由调用者自行实现。

这种风格的优点包括:

  • 能够针对界面特点,仅封装不变的模板代码部分,能够最大程度保留Template编程风格。
  • 因为表单项都是通过Slot实现,不会导致这个表单组件随着应用的场景增多和变得越来越复杂,复杂度会基本保持不变。

其缺点主要在于:

  • 需要掌握JSX语法(注意JSX仅用于封装组件,调用这个组件使用Template语法即可)。

以下是风格二的代码实现示例。

表单项组件封装代码如下:

<script>
export default {
  functional: true,
  name: "DynFormItemsFunction",
  props: {
    formItemConfigArr: {
      type: Array,
      required: true,
    },
  },
  render(h, context) {
    // console.log(context);
    const { props, scopedSlots } = context;
    const { formItemConfigArr } = props;
    // 获得插槽里的 vNodes
    const vNodes = scopedSlots.default();
    return vNodes.map((node, idx) => {
      const formItemConfig = formItemConfigArr[idx];
      return (
        <el-form-item prop={formItemConfig.prop} rules={formItemConfig.rules}>
          <span slot="label" className="o-custom-label">
            {formItemConfig.label}
          </span>
          {node}
        </el-form-item>
      );
    });
  },
};
</script>

<style scoped>
.o-custom-label {
  color: blue;
}
</style>

调用动态表单组件的代码示例如下:

<template>
  <div>
    <el-form :model="formData" inline :validate-on-rule-change="false">
      <!-- DynFormItemsFunction 这个组件,仅封装了el-form-item的逻辑 -->
      <DynFormItemsFunction :form-item-config-arr="formItemConfigArrComp">
        <!-- 
        注意组件的循环是在slot中进行的,el-form-item的包装逻辑,
        交给了DynFormItemsFunction组件实现
        -->
        <template v-for="(formItemConfig, idx) in formItemConfigArrComp">
          <el-input
            v-if="formItemConfig.itemType === 'input'"
            v-model="formData[formItemConfig.prop]"
            :key="idx"
          ></el-input>
          <el-select
            v-else-if="formItemConfig.itemType === 'select'"
            v-model="formData.sex"
            :key="idx"
          >
            <template v-if="formItemConfig.optionArr">
              <el-option
                v-for="option in formItemConfig.optionArr"
                :key="option.value"
                :value="option.value"
                :label="option.label"
              ></el-option>
            </template>
          </el-select>
        </template>
      </DynFormItemsFunction>
    </el-form>
  </div>
</template>

<script>
import DynFormItemsFunction from "@/components/form/dyn/DynFormItemsFunction";
export default {
  name: "FormTemplate",
  components: {
    DynFormItemsFunction,
  },
  props: {
    count: {
      type: Number,
      default: 50,
    },
  },
  data() {
    /*
    完整的表单配置放在data中,数据中的元素个数不会变,
    动态值也填充到formItemConfigArr对应元素中(如性别的下拉选项值),
    formItemConfigArr只需要关注完整表单需要哪些表单项字段,
    以及下拉选项数据的填充,无需考虑表单项字段的显示/隐藏逻辑控制
    */
    const formItemConfigArr = [
      {
        prop: "sex",
        label: "性别",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "name",
        label: "姓名",
        itemType: "input",
        rules: [{ required: true, message: "该项必填", trigger: "change" }],
      },
      {
        prop: "school",
        label: "学校",
        itemType: "input",
        rules: [],
      },
    ];
    // for (let i = 0; i < this.count; i++) {
    //   formItemConfigArr.push({
    //     prop: "name" + i,
    //     label: "姓名" + i,
    //     itemType: "input",
    //   });
    // }
    return {
      formData: {
        name: null,
        sex: null,
        school: null,
      },
      formItemConfigArr,
    };
  },
  computed: {
    /*
    表单项的显示/隐藏通过计算属性实现,可以认为计算属性就只关注控制哪些表单项需要显示,
    哪些需要隐藏,就可以了。算是一种职责分离
    */
    formItemConfigArrComp() {
      return this.formItemConfigArr.filter((item) => {
        if (this.formData.sex === 3 && item.prop === "name") {
          return null;
        }
        return item;
      });
    },
  },
  created() {
    this.loadOptions(1500);
  },
  methods: {
    loadOptions(timeout) {
      setTimeout(() => {
        const item = this.formItemConfigArr.find((item) => item.prop === "sex");
        if (item) {
          const sexOptionArr = new Array(10)
            .fill(true)
            .map((item, idx) => ({ value: idx, label: "选项:" + idx }));
          item.optionArr = sexOptionArr;
        }
      }, timeout);
    },
  },
};
</script>

<style scoped lang="scss">
.js-validate-form ::v-deep(.is-error .o-show-data) {
  color: red;
}
</style>

到此这篇关于Vue2实现封装动态表单组件的文章就介绍到这了,更多相关Vue封装表单组件内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章