一. 前端组件化的意义
在前端框架搭建之初,通常会封装 通用UI组件 和 通用业务组件 。
(为什么用vue2举例?因为最近接手了一个vue2项目,正在进行重构🤣🤣🤣🤣🤣)
举几个例子:
-
小A同学写了一个页面容器样式,但并未提取到全局组件或者公共样式内,后面大家为了保持样式一致,直接复制粘贴了他的代码。后来该样式在项目内写的次数越来越多。再后来,领导要求需要添加标题,小A便在该节点下追加了标题并写了样式,其他同事纷纷效仿......项目越来越臃肿,维护点也越来越多。
如果最初小A封装了一个容器组件,那么直接在组件内维护相应内容即可,是不是确实的减少了维护成本!
-
小B同学做了一个附件上传功能,其他用到的地方一顿赋值粘贴。领导要求,附件整体逻辑要进行改造,需要通过配置附件名称、所属模块、编码、是否必传、附件大小等等,根据配置内容展示附件上传列表、附件详情列表及附件下载。按照目前写法,那么要魔改n处!!!
如果附件上传功能设计之初,就封装了通用的组件,或许只改一个组件就能够解决😪。
前端组件化的意义
前端组件化的意义在于将前端代码拆分为独立的、可复用的组件,提高代码复用性、开发效率、代码可维护性和协作能力,同时也提升了用户体验和项目的可扩展性。通过组件化,可以更好地应对复杂的前端开发需求,并加速项目的开发进程。
-
代码复用:通过组件化,可以将页面中的不同部分抽象为独立的组件,提高代码的复用性。这样,可以在不同的页面或不同的项目中重复使用这些组件,避免重复编写相同的代码,减少了开发和维护的工作量。
-
提高开发效率:组件化使得团队成员可以并行开发,每个成员可负责开发和维护自己专属的组件。这种并行开发的方式可以提高开发效率,加快项目的上线速度。同时,组件化也提高了代码的可测试性,可进行单元测试和集成测试,保证代码质量。
-
提升可维护性:通过组件化,将代码拆分为独立的组件,可降低代码的耦合性。这使得代码更容易理解和维护,减少了BUG的产生和修复成本。在修改某个组件时,也不会对其他组件产生影响,提高了代码的可维护性。
-
统一管理和样式:通过组件化,可以统一管理组件的样式,保持整个项目的风格一致性。此外,可以定义通用的交互行为和功能,使得各个组件之间可以相互配合工作,提供更好的用户体验。
-
提升开发协作:组件化为团队提供了更好的协作方式。每个成员负责自己的组件开发,避免了代码冲突和团队协作的问题。同时,组件化也促进了交流和知识共享,提升了团队的整体水平。
二. 以vue2项目为例,封装基于elementui的表单组件
1、封装思路:
- 使用 Vue.js 框架构建组件:代码采用 Vue.js 框架封装成一个可复用的组件,方便在其他地方使用。使用component...is动态渲染组件。
- 对element ui表单组件进行拓展,并且配置项尽量与element ui Api项保持一致,便于理解使用。
- 数据驱动:通过接收传入的 props 属性(
formRef
、model
、formItemConfig
和rules
)实现组件和表单数据的双向绑定,从而保持表单项的实时同步。 - 计算属性动态渲染组件:使用计算属性
isComponentName
根据表单项配置的组件类型动态决定渲染何种类型的表单项组件,提高代码的灵活性和可扩展性。 - 表单验证和交互处理:封装了 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、实现要点:
- props 和 computed:通过 props 接收外部传入的数据,computed 计算属性实现根据表单项的配置动态渲染对应的组件类型和占位符。
- 表单项的配置:使用一个数组类型的
formItemConfig
属性传入表单的配置项,包含类型、标签、占位符等信息,实现动态的表单项渲染。 - 表单数据和验证处理:通过双向绑定的方式实现表单项和表单数据的同步更新,并使用
$refs
属性来进行表单的验证。根据验证结果执行回调函数进行后续处理。 - 事件处理:处理表单项的点击和数据改变事件,实现交互逻辑的处理。通过
handleClick
和handleChange
方法实现对应的事件处理。
4、 API
props:
- formData 是一个对象,用于存储表单数据;
- formRules 是一个对象,用于配置表单验证规则。
- formConfig 是一个数组,用于配置表单项;数组中每一项配置可参考
element ui
表单组件配置项。其中 component 是必填项,用于指定表单元素的类型
component:
- 输入框(默认为 el-input,可不传)
- 选择器(SelectForm),后面附源码
- 单选框(RadioGroupForm)其中配置项 isButton 为 true,默认展示为按钮样式,否则展示为单选框样式,后面附源码
- 复选框(CheckboxGroupForm),后面附源码
- 点击选择(ClickForm),后面附源码
- 字符串类型的时间组件("el-date-picker"、"el-input-number"等)
- type: "textarea"为多行文本框
- 自定义传入自己的组件即可
options:
- 下拉、单选、复选框等组件的选项
onChange:
- 用于监听表单元素值改变的事件,入参为表单元素的值
- 特殊的,通过 onChange 可以控制表单项的隐藏和显示,或控制级联表单的联动,案例中有呈现;
title:
- 表单项的标题
placeholder:
- 表单项的占位符,输入类型的默认为"请选择 label 的值",选择器类型的默认为"请选择 label 的值",特殊的可以自己传入
hidden:
- 是否隐藏表单项,默认为 false
- 可以配合 onChange 来控制其他表单项的隐藏和显示,可以查看使用案例。
5、技术要点
-
动态组件: 使用 Vue 的动态组件功能
<component :is="...">
来根据配置动态生成不同的表单元素。 -
v-model: 使用
v-model
指令进行双向数据绑定,使得表单元素的值能够与数据模型同步。 -
计算属性: 使用计算属性
computed
来动态计算表单元素的类型和占位符。 -
事件处理: 提供了
handleClick
、handleChange
和changeValue
方法来处理表单元素的点击、改变和输入事件。
三、 其他表单组件
其他类型组件可以根据项目特点进行个性化封装,以下为一些示例。
下拉组件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>
四、优点及缺点
组件封装有利有弊,需要根据自己团队情况和项目情况判断封装程度和能力。通过以上封装组件后,在项目中使用的优缺点如下:
优点:
- 代码复用性高:将动态表单封装成组件,可以在多个项目或多个页面中复用该组件,减少代码冗余,提高开发效率。
- 维护成本低:对动态表单进行封装,可以将参数化配置与业务逻辑分离,便于维护和升级。
- 代码组织结构清晰:将动态表单封装成组件,可以使代码结构更加清晰,易于理解和维护。
缺点:
- 抽象程度高:在将动态表单封装成组件时,需要一定的抽象能力,将动态表单中的通用逻辑抽象出来,需要一定的经验。
- 依赖组件库:如果使用第三方组件库中的组件,需要依赖相应的组件库,增加了项目的依赖。同时,如果组件库中的组件与项目中的其它组件存在冲突,可能会造成一定的影响。
- 适用范围受限:动态表单组件适用于表单比较简单的场景,如果表单非常复杂,可能需要针对具体场景开发组件或增加相应的逻辑,使得适用范围相对受限。
- 组件冗余度高:若需求多样性要求较高,组件内会为兼容各种场景做适应,会导致组件冗余度高,复杂度大。
适用场景:
-
团队成员能力差距较大:在团队成员能力差距较大情况下,通过组件封装比较方便的进行统一要求风格样式及编写规范,提高代码可维护性。
-
跨团队或跨项目开发团队:在跨团队或跨项目开发中,通过封装动态表单组件,可以提高组件复用性,减少团队间协调成本和缩短开发周期。
-
需求频繁变化的项目:在需求频繁变化的项目中,通过动态表单组件封装,可以将表单配置信息与代码逻辑分离,方便快速响应需求变化,降低维护成本。
以上内容若有错误,请指正🌹