前端组件封装-表单组件(vue2+Element UI)

一. 前端组件化的意义

在前端框架搭建之初,通常会封装 通用UI组件通用业务组件

(为什么用vue2举例?因为最近接手了一个vue2项目,正在进行重构🤣🤣🤣🤣🤣)

举几个例子:

  1. 小A同学写了一个页面容器样式,但并未提取到全局组件或者公共样式内,后面大家为了保持样式一致,直接复制粘贴了他的代码。后来该样式在项目内写的次数越来越多。再后来,领导要求需要添加标题,小A便在该节点下追加了标题并写了样式,其他同事纷纷效仿......项目越来越臃肿,维护点也越来越多。

    如果最初小A封装了一个容器组件,那么直接在组件内维护相应内容即可,是不是确实的减少了维护成本!

  2. 小B同学做了一个附件上传功能,其他用到的地方一顿赋值粘贴。领导要求,附件整体逻辑要进行改造,需要通过配置附件名称、所属模块、编码、是否必传、附件大小等等,根据配置内容展示附件上传列表、附件详情列表及附件下载。按照目前写法,那么要魔改n处!!!

    如果附件上传功能设计之初,就封装了通用的组件,或许只改一个组件就能够解决😪。

前端组件化的意义

前端组件化的意义在于将前端代码拆分为独立的、可复用的组件,提高代码复用性、开发效率、代码可维护性和协作能力,同时也提升了用户体验和项目的可扩展性。通过组件化,可以更好地应对复杂的前端开发需求,并加速项目的开发进程。

  1. 代码复用:通过组件化,可以将页面中的不同部分抽象为独立的组件,提高代码的复用性。这样,可以在不同的页面或不同的项目中重复使用这些组件,避免重复编写相同的代码,减少了开发和维护的工作量。

  2. 提高开发效率:组件化使得团队成员可以并行开发,每个成员可负责开发和维护自己专属的组件。这种并行开发的方式可以提高开发效率,加快项目的上线速度。同时,组件化也提高了代码的可测试性,可进行单元测试和集成测试,保证代码质量。

  3. 提升可维护性:通过组件化,将代码拆分为独立的组件,可降低代码的耦合性。这使得代码更容易理解和维护,减少了BUG的产生和修复成本。在修改某个组件时,也不会对其他组件产生影响,提高了代码的可维护性。

  4. 统一管理和样式:通过组件化,可以统一管理组件的样式,保持整个项目的风格一致性。此外,可以定义通用的交互行为和功能,使得各个组件之间可以相互配合工作,提供更好的用户体验。

  5. 提升开发协作:组件化为团队提供了更好的协作方式。每个成员负责自己的组件开发,避免了代码冲突和团队协作的问题。同时,组件化也促进了交流和知识共享,提升了团队的整体水平。

二. 以vue2项目为例,封装基于elementui的表单组件

1、封装思路:

  1. 使用 Vue.js 框架构建组件:代码采用 Vue.js 框架封装成一个可复用的组件,方便在其他地方使用。使用component...is动态渲染组件。
  2. 对element ui表单组件进行拓展,并且配置项尽量与element ui Api项保持一致,便于理解使用。
  3. 数据驱动:通过接收传入的 props 属性(formRefmodelformItemConfigrules)实现组件和表单数据的双向绑定,从而保持表单项的实时同步。
  4. 计算属性动态渲染组件:使用计算属性 isComponentName 根据表单项配置的组件类型动态决定渲染何种类型的表单项组件,提高代码的灵活性和可扩展性。
  5. 表单验证和交互处理:封装了 validate 方法用于表单验证,根据验证结果执行回调函数。同时,定义了 handleClick 和 handleChange 方法处理表单项的点击和数据改变事件,方便与其他逻辑进行交互。

2、代码实现

FreeForm.vue

js 复制代码
<template>
  <!-- 表单组件核心代码 -->
  <el-form
    class="freeForm"
    :ref="formRef"
    :model="model"
    v-bind="$attrs"
    :rules="rules"
    @validate="$handleFormValidate"
  >
    <el-row :gutter="15">
      <!-- 显示hidden为false的表单项 -->
      <el-col
        v-for="(item, index) in formItemConfig"
        :key="index"
        :span="item.span"
        v-show="!item.hidden"
      >
        <div class="freeFormItem">
          <!-- 处理标题 -->
          <p v-if="item.title" class="cgtitle">{{ item.title }}</p>
          <el-form-item
            v-else
            :label="item.label"
            :prop="item.prop"
            style="width: 100%"
          >
            <!-- 动态渲染组件 -->
            <component
              :is="isComponentName(item)"
              v-model="model[item.prop]"
              :placeholder="placeholder(item)"
              v-bind="item"
              :style="{ width: item.width }"
              @input="changeValue(item, $event)"
              @click="handleClick(item, $event)"
              @change="handleChange(item, $event)"
            />
          </el-form-item>
        </div>
      </el-col>
    </el-row>
  </el-form>
</template>

<script>
/**
 * @desc 表单组件
 * @param {Object} formRef - el-form 的 ref 名称
 * @param {Object} model - 表单数据模型
 * @param {Object} formItemConfig - el-form-item 配置项
 * @param {Object} rules - el-form-item 验证规则
 */
export default {
  props: {
    // 表单引用名称
    formRef: {
      type: String,
      default: "formRef",
    },
    // 表单数据模型
    model: {
      type: Object,
      default: () => ({}),
    },
    // 表单项配置
    formItemConfig: {
      type: Array,
      default: () => [],
    },
    // 表单验证规则
    rules: {
      type: Object,
      default: () => ({}),
    },
    modelCode: {
      type: String,
      default: "",
    },
  },
  computed: {
    /**
     * 根据组件类型获取需要渲染的组件名称
     */
    isComponentName() {
      return (item) => {
        if (item.component === "el-select") {
          return "SelectForm";
        } else if (item.component === "radio") {
          return "RadioGroupForm";
        } else if (item.component === "checkbox") {
          return "CheckboxGroupForm";
        } else {
          return item.component || "el-input";
        }
      };
    },
    /**
     * 根据表单项配置获取占位符
     */
    placeholder() {
      return (item) => {
        return item.component === "el-input"
          ? `请输入${item.label || ""}`
          : `请选择${item.label || ""}`;
      };
    },
  },
  methods: {
    /**
     * 验证表单并执行回调函数
     * @param {Function} cb - 表单验证通过后的回调函数
     * @returns {boolean} - 表单验证结果
     */
    validate(cb) {
      this.$refs[this.formRef].validate((valid) => {
        cb(valid, this.model);

        if (valid) {
          // 如果表单验证通过,执行提交操作
        } else {
          // 如果表单验证失败,处理失败情况
          return false;
        }
      });
    },
    /**
     * 处理表单项的点击事件
     * @param {Object} item - 当前点击的表单项配置
     */
    handleClick(item, e) {
      // 处理数据改变的逻辑
      item.onClick ? item.onClick(e) : () => {};
    },
    //change型式的回调
    handleChange(item, e) {
      item.onChange ? item.onChange(e) : () => {};
    },
    /**
     * 更新表单数据模型到父组件
     */
    changeValue(item, e) {
      this.$emit("input", e);
    },
  },
};
</script>

使用案例:

js 复制代码
<template>
  <div class="wapper">
    <free-form
      ref="form"
      formRef="freeForm"
      :model="formData"
      :formItemConfig="formItemConfig"
      :rules="rules"
      label-width="150px"
      label-position="top"
    />
    <el-button type="primary" @click="submitForm">提交</el-button>
  </div>
</template>


<script>
import FreeForm from "@/components/FreeForm";
import SelectForm from "@/components/global/SelectForm.vue";
import ClickForm from "@/components/global/ClickForm.vue";
import CheckboxGroupForm from "@/components/global/CheckboxGroupForm.vue";
import RadioGroupForm from "@/components/global/RadioGroupForm.vue";

export default {
  components: {
    FreeForm,
    SelectForm,
    ClickForm,
    CheckboxGroupForm,
    RadioGroupForm,
  },
  data() {
    return {
      // 表单数据
      formData: {
        username: "",
        password: "",
        select: "",
        sex: "",
        love: [],
      },
      // el-form-item 配置项
      formItemConfig: [
        {
          label: "用户名",
          prop: "username",
          component: "el-input", // el-input可以省略,默认使用el-input
          placeholder: "请输入用户名", // placeholder可以省略,默认显示"请输入+label"
          span: 12, // 使用栅格布局
        },
        {
          label: "密码",
          prop: "password",
          span: 12, // 使用栅格布局
        },
        {
          label: "下拉",
          prop: "select",
          component: SelectForm, // 可以传入任意组件
          placeholder: "请输入选择",
          clearable: true,
          width: "100%", // 设置宽度
          options: [
            { label: "选项1", value: "option1" },
            { label: "选项2", value: "option2" },
          ],
          onChange: (value) => {
            console.log(value);
          },
        },
        {
          label: "点击选择",
          prop: "selectclick",
          component: ClickForm,
          readonly: true,
          span: 12,
          onClick: () => {
            console.log("点击了");
          },
        },
        {
          label: "时间选择",
          prop: "time",
          component: "el-date-picker",
          clearable: true,
          type: "month",
          format: "yyyy-MM",
          valueFormat: "yyyy-MM",
          span: 12,
          width: "100%",
        },
        {
          label: "性别",
          prop: "sex",
          span: 12, // 支持栅格布局
          component: RadioGroupForm, // 可以传入任意组件
          options: [
            {
              label: "男",
              value: 1,
            },
            {
              label: "女",
              value: 2,
            },
          ],
          onChange: (e) => {
            console.log(e);
          },
        },
        {
          label: "兴趣爱好",
          prop: "love",
          span: 12, // 支持栅格布局
          component: CheckboxGroupForm, // 可以传入任意组件
          options: [
            {
              label: "读书",
              value: 1,
            },
            {
              label: "写字",
              value: 2,
            },
            {
              label: "听歌",
              value: 4,
            },
          ],
          onChange: (e) => {
            console.log(e);
          },
        },
        {
          title: "标题-级联",
        },
        {
          label: "级联类型",
          prop: "major",
          component: RadioGroupForm,
          isButton: true,
          clearable: true,
          span: 12,
          options: [
            {
              label: "小学",
              value: "primary",
            },
            {
              label: "初中",
              value: "junior",
            },
          ],
          onChange: () => {
            this.changeMajor("change");
          },
        },
        {
          label: "级联类型2",
          prop: "majorType",
          span: 12,
          component: RadioGroupForm,
          isButton: true,
          clearable: true,
          options: [],
        },
        {
          label: "是否展示菜单",
          prop: "researchType",
          span: 12,
          component: RadioGroupForm,
          options: [
            {
              label: "是",
              value: "y",
            },
            {
              label: "否",
              value: "n",
            },
          ],
          onChange: (e) => {
            this.changedevelopmentMethods(e);
          },
        },
        {
          label: "菜单",
          prop: "developmentMethods",
          span: 12,
          component: RadioGroupForm,
          isButton: true,
          clearable: true,
          hidden: true,
          options: [
            {
              label: "菜单1",
              value: "menu1",
            },
            {
              label: "菜单二",
              value: "menu2",
            },
          ],
        },
      ],
      // el-form-item 验证规则
      rules: {
        username: {
          required: true,
          message: "请输入用户名",
          trigger: "blur",
        },
      },
    };
  },
  methods: {
    submitForm() {
      // 调用 FreeForm 组件的 validate() 方法,验证表单
      this.$refs.form.validate((valid, formData) => {
        console.log(valid, formData);
      });
    },
    // 级联类型
    changeMajor(type) {
      const json = {
        primary: [
          {
            label: "数学",
            value: 1,
          },
          {
            label: "语文",
            value: 2,
          },
        ],
        junior: [
          {
            label: "英语",
            value: 5,
          },
          {
            label: "生物",
            value: 7,
          },
        ],
      };
      if (type === "change") {
        this.formData.majorType = "";
      }
      this.formItemConfig.forEach((item) => {
        if (item.prop === "majorType") {
          item.options = json[this.formData.major];
        }
      });
    },
    // 显示隐藏类型
    changedevelopmentMethods(e) {
      this.formItemConfig.forEach((item) => {
        if (item.prop === "developmentMethods") {
          item.hidden = e === "n" ? true : false;
        }
      });
    },
  },
};
</script>

<style>
.wapper {
  padding: 10%;
  background: #fff;
}
</style>

3、实现要点:

  1. props 和 computed:通过 props 接收外部传入的数据,computed 计算属性实现根据表单项的配置动态渲染对应的组件类型和占位符。
  2. 表单项的配置:使用一个数组类型的 formItemConfig 属性传入表单的配置项,包含类型、标签、占位符等信息,实现动态的表单项渲染。
  3. 表单数据和验证处理:通过双向绑定的方式实现表单项和表单数据的同步更新,并使用 $refs 属性来进行表单的验证。根据验证结果执行回调函数进行后续处理。
  4. 事件处理:处理表单项的点击和数据改变事件,实现交互逻辑的处理。通过 handleClickhandleChange 方法实现对应的事件处理。

4、 API

props:

  1. formData 是一个对象,用于存储表单数据;
  2. formRules 是一个对象,用于配置表单验证规则。
  3. formConfig 是一个数组,用于配置表单项;数组中每一项配置可参考 element ui 表单组件配置项。其中 component 是必填项,用于指定表单元素的类型

component:

  1. 输入框(默认为 el-input,可不传)
  2. 选择器(SelectForm),后面附源码
  3. 单选框(RadioGroupForm)其中配置项 isButton 为 true,默认展示为按钮样式,否则展示为单选框样式,后面附源码
  4. 复选框(CheckboxGroupForm),后面附源码
  5. 点击选择(ClickForm),后面附源码
  6. 字符串类型的时间组件("el-date-picker"、"el-input-number"等)
  7. type: "textarea"为多行文本框
  8. 自定义传入自己的组件即可

options:

  • 下拉、单选、复选框等组件的选项

onChange:

  • 用于监听表单元素值改变的事件,入参为表单元素的值
  • 特殊的,通过 onChange 可以控制表单项的隐藏和显示,或控制级联表单的联动,案例中有呈现;

title:

  • 表单项的标题

placeholder:

  • 表单项的占位符,输入类型的默认为"请选择 label 的值",选择器类型的默认为"请选择 label 的值",特殊的可以自己传入

hidden:

  • 是否隐藏表单项,默认为 false
  • 可以配合 onChange 来控制其他表单项的隐藏和显示,可以查看使用案例。

5、技术要点

  1. 动态组件: 使用 Vue 的动态组件功能 <component :is="..."> 来根据配置动态生成不同的表单元素。

  2. v-model: 使用 v-model 指令进行双向数据绑定,使得表单元素的值能够与数据模型同步。

  3. 计算属性: 使用计算属性 computed 来动态计算表单元素的类型和占位符。

  4. 事件处理: 提供了 handleClickhandleChangechangeValue 方法来处理表单元素的点击、改变和输入事件。

三、 其他表单组件

其他类型组件可以根据项目特点进行个性化封装,以下为一些示例。

下拉组件SelectForm

  • 基于Element UI库,同时通过v-bind指令将父组件传递过来的属性绑定到el-select上,通过v-on指令将父组件传递过来的事件绑定到el-select上,同时使用v-model指令将组件内部的modelValue与value属性进行双向绑定。

  • 使用props属性定义了value和options两个属性,其中value属性是默认值,options属性是选项数据,默认为空数组。

js 复制代码
<template>
  <el-select v-bind="$attrs" v-on="$listeners" v-model="modelValue">
    <el-option
      v-for="(option, index) in options"
      :key="index"
      :label="option.label"
      :value="option.value"
    >
    </el-option>
  </el-select>
</template>

<script>
export default {
  props: {
    value: {
      required: true,
    },
    options: {
      type: Array,
      default: () => [],
    },
  },
  computed: {
    modelValue: {
      get() {
        return this.value;
      },
      set(val) {
        this.$emit("input", val);
      },
    },
  },
};
</script>
    
    

单选组件RadioGroupForm

  • 基于Element UI库,封装单选组件,使用了v-model指令将组件内部的internalValue与value属性进行双向绑定。使用了v-on指令绑定了父组件的事件监听器,确保内部数据的改变向外部通知,v-bind指令绑定了attrs对象的属性。
  • isButton属性是否为true,如果是,则渲染el-radio-button元素。
  • 使用props属性定义了value和options两个属性,其中value属性是默认值,options属性是选项数据,默认为空数组。

配置了两种样式,一种是常规单选,一种是按钮样式。

js 复制代码
    <template>
  <el-radio-group
    v-model="internalValue"
    v-on="$listeners"
    v-bind="$attrs"
    size="small"
    class="radioGroupForm"
    :class="$attrs.isButton ? 'is-button' : ''"
  >
    <template v-if="$attrs.isButton">
      <el-radio-button
        v-for="(option, index) in options"
        :key="index"
        :label="option.value"
      >
        {{ option.label }}
      </el-radio-button>
    </template>
    <template v-else>
      <el-radio
        v-for="(option, index) in options"
        :key="index"
        :label="option.value"
      >
        {{ option.label }}
      </el-radio>
    </template>
  </el-radio-group>
</template>

<script>
export default {
  props: {
    value: [String, Number],
    options: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      internalValue: this.value,
    };
  },
  watch: {
    value(newVal) {
      console.log("radioGroupForm", newVal);
      this.internalValue = newVal;
    },
    internalValue(newVal) {
      this.$emit("input", newVal);
    },
  },
};
</script>

复选组件CheckboxGroupForm

  • 基于Element UI库,封装复选组件,使用v-model指令将组件内部的internalValue与value属性进行双向绑定。通过v-for指令遍历父组件传递过来的options数组,渲染出对应的多选项。

  • 使用props属性定义了value和options两个属性,其中value属性是默认值,options属性是选项数据,默认为空数组。

  • 使用了v-on指令绑定了父组件的事件监听器,确保内部数据的改变向外部通知,v-bind指令绑定了attrs对象的属性。

js 复制代码
<template>
  <el-checkbox-group v-model="internalValue" v-on="$listeners" v-bind="$attrs">
    <el-checkbox
      v-for="option in options"
      :key="option.value"
      :label="option.value"
    >
      {{ option.label }}
    </el-checkbox>
  </el-checkbox-group>
</template>

<script>
export default {
  props: {
    value: Array,
    options: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      internalValue: this.value,
    };
  },
  watch: {
    value(newVal) {
      this.internalValue = newVal;
    },
  },
};
</script>

点击选择组件ClickForm

  • 基于 Element UI 组件库的 Vue 组件,实现点击选择样式,点击组件触发 openDialog 方法,通过 $emit 事件向父组件发送消息。
js 复制代码
<template>
  <el-input
    v-model="value"
    readonly
    @click.stop.native="openDialog"
    :placeholder="`请选择${$attrs.label || ''}`"
  >
    <template slot="append">
      <el-link @click.stop="openDialog">选择</el-link>
    </template>
  </el-input>
</template>

<script>
export default {
  props: ["value"],
  methods: {
    openDialog() {
      this.$emit("click");
    },
  },
};
</script>

四、优点及缺点

组件封装有利有弊,需要根据自己团队情况和项目情况判断封装程度和能力。通过以上封装组件后,在项目中使用的优缺点如下:

优点:

  1. 代码复用性高:将动态表单封装成组件,可以在多个项目或多个页面中复用该组件,减少代码冗余,提高开发效率。
  2. 维护成本低:对动态表单进行封装,可以将参数化配置与业务逻辑分离,便于维护和升级。
  3. 代码组织结构清晰:将动态表单封装成组件,可以使代码结构更加清晰,易于理解和维护。

缺点:

  1. 抽象程度高:在将动态表单封装成组件时,需要一定的抽象能力,将动态表单中的通用逻辑抽象出来,需要一定的经验。
  2. 依赖组件库:如果使用第三方组件库中的组件,需要依赖相应的组件库,增加了项目的依赖。同时,如果组件库中的组件与项目中的其它组件存在冲突,可能会造成一定的影响。
  3. 适用范围受限:动态表单组件适用于表单比较简单的场景,如果表单非常复杂,可能需要针对具体场景开发组件或增加相应的逻辑,使得适用范围相对受限。
  4. 组件冗余度高:若需求多样性要求较高,组件内会为兼容各种场景做适应,会导致组件冗余度高,复杂度大。

适用场景:

  1. 团队成员能力差距较大:在团队成员能力差距较大情况下,通过组件封装比较方便的进行统一要求风格样式及编写规范,提高代码可维护性。

  2. 跨团队或跨项目开发团队:在跨团队或跨项目开发中,通过封装动态表单组件,可以提高组件复用性,减少团队间协调成本和缩短开发周期。

  3. 需求频繁变化的项目:在需求频繁变化的项目中,通过动态表单组件封装,可以将表单配置信息与代码逻辑分离,方便快速响应需求变化,降低维护成本。

以上内容若有错误,请指正🌹

相关推荐
祈澈菇凉2 小时前
Webpack的基本功能有哪些
前端·javascript·vue.js
小纯洁w2 小时前
Webpack 的 require.context 和 Vite 的 import.meta.glob 的详细介绍和使用
前端·webpack·node.js
想睡好2 小时前
css文本属性
前端·css
qianmoQ2 小时前
第三章:组件开发实战 - 第五节 - Tailwind CSS 响应式导航栏实现
前端·css
zhoupenghui1683 小时前
golang时间相关函数总结
服务器·前端·golang·time
White graces3 小时前
正则表达式效验邮箱格式, 手机号格式, 密码长度
前端·spring boot·spring·正则表达式·java-ee·maven·intellij-idea
庸俗今天不摸鱼3 小时前
Canvas进阶-4、边界检测(流光,鼠标拖尾)
开发语言·前端·javascript·计算机外设
bubusa~>_<3 小时前
解决npm install 出现error,比如:ERR_SSL_CIPHER_OPERATION_FAILED
前端·npm·node.js
yanglamei19623 小时前
基于Python+Django+Vue的旅游景区推荐系统系统设计与实现源代码+数据库+使用说明
vue.js·python·django
流烟默4 小时前
vue和微信小程序处理markdown格式数据
前端·vue.js·微信小程序