《如何写出高质量的前端代码》学习笔记
表单开发是前端开发中不可避免的一个重要话题,尤其是在管理系统中。表单开发不仅占用大量时间,还容易出现可读性、可维护性差的问题。我们来探讨表单开发中的常见问题及解决方案。
表单组件的定义
表单组件是指用于处理表单数据的可复用组件。开发表单组件时,应遵循一定的开发范式,主要分为受控组件和非受控组件。
受控组件
受控组件的内部状态由外部属性控制,必须满足以下条件:
- 存在一个名为
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个字符'
}
]
复杂业务表单的开发
思路
- 拆分表单: 将复杂表单拆分为多个子表单,每个子表单负责一部分逻辑。
- 数据流管理: 使用受控或非受控组件的方式管理子表单的数据流。
- 表单校验: 每个子表单提供一个
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方法。
建议优先开发受控组件,以简化使用。通过提取表单项组件、使用配置表单、统一校验规则等方式,提高表单开发的质量和效率。对于复杂表单,采用分治思想进行拆分和管理。