前言
大家可能有过业务组件库这类工具的使用经历,基于它提供的可复用组件可以非常高效地搭建一些页面,但由于这类开发工具隐藏了实现细节,提供好API让我们可以拿来即用,然而往往会有一种用轮子简单,扩展轮子却很难的现象,的确大部分情况下照葫芦画瓢地搬运可以应对大部分需求,可当现有的轮子不能满足需求不得不做出变化时,我们则会做出不同的选择:有一部分人会选择另辟蹊径遇事不决setTimeout实现需求,有一部分人会选择换另一个轮子,而还有一部分人会选择深挖源码理解轮子原理以便后续做扩展。
在后台管理端项目中,表单类组件通常会被大量使用,比如输入框(Input)、计数器(InputNumber)、选择器(Select)、单选(Radio)、多选(Checkbox)、穿梭框(Transfer)、上传(Upload)、自定义表单组件等等,如今市面上开源的组件库都提供了这些组件,比如 Element、Ant Design、Arco Design、NUTUI等等
以前在开发表单类页面时,会经常用到数据校验,而如果对于以上每一个表单组件类型,都要在使用时绑定上点击事件或者失焦事件去做校验逻辑,那样的原生写法就会显得很冗余难以维护,因此我们需要一种更高效优雅的开发方式,即通过简单配置就能实现对这类表单组件的校验,也就是本文要讲的Form表单组件,本文我们将通过阅读Element源码来理解一个可配置化的表单验证功能是如何实现的,希望能对从事相关开发的同学有所帮助和启发。
表单组件概览
在管理端项目中,如果我们要用Element组件库去开发一个像这样的带校验效果的表单,那么代码的基本结构通常看起来会像下面这样
<template>
<el-form :model="ruleForm" :rules="rules" label-width="100px" ref="ruleForm" class="form-wrapper">
<el-form-item label="用户名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="ruleForm.password"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
ruleForm: {
username: "",
password: ""
},
rules: {
username: [{ required: true, message: "请输入用户名", trigger: "change" }],
password: [{ required: true, validator: this.validatePass, trigger: "change" }]
}
};
},
methods: {
validatePass(rule, value, callback) {
if (value === "") {
callback(new Error("请输入密码"));
} else {
callback();
}
},
submitForm() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
alert("submit!");
} else {
console.log("error submit!!");
}
});
},
resetForm() {
this.$refs.ruleForm.resetFields();
},
},
};
</script>
<style scoped>
.form-wrapper {
width: 500px;
}
</style>
从代码结构可以看出,在使用表单类组件时,想要应用配置化的表单校验功能,需要搭配el-form和el-form-item组件来使用:一个el-form包裹多个el-form-item,每个el-form-item再包裹一个表单组件,把表单验证规则对象数组传递给el-form,把表单需要校验的规则名给el-form-item绑定上,这样在表单组件触发change/blur事件时,就可以自动执行表单验证了,本质上是把原本应该写在表单组件上的事件,通过某种方式转移给了表单组件外层的el-form和el-form-item中去
现在我们思考如下几个问题:
- 表单组件如何通知el-form-item做校验?
- el-form-item组件如何拿到校验的规则和需要校验的数据?
- el-form组件如何做全表单校验和重置校验状态?
表单组件实现原理
表单组件通知el-form-item执行校验
以el-input输入框表单组件为例,会在触发blur事件时 或者 在双向绑定的value字段发生变化时调用this.dispatch方法,这个方法是通过mixins混入进来的
这个方法会依据给定的组件名不断寻找祖先组件【ElFormItem表单域组件】找到后向该组件派发 el.form.blur 或者 el.form.change 事件并带上变化后的表单字段值,接下来我们去el-form-item中查看这两个事件的定义
el-form-item注册校验事件
在el-form-item的源码中,在组件mounted的时候会注册监听事件 el.form.blur 和 el.form.change,底层都是调用 this.validate方法
el-form-item获取校验规则和需要校验的数据
this.validate方法首先会去拿祖先组件el-form组件里的model表单数据和rules校验规则对象,通过 el-form-item的 prop表单校验字段从 model和 rules中取出对应的验证规则和表单字段值,然后通过第三方校验库 async-validator校验并得出校验结果,最后把验证结果通过注入的 elForm 的 validate事件告诉 el-form 组件
el-form实现全表单的校验和重置
实现整表单的校验和重置主要是遍历el-form-item子组件并复用子组件的校验和重置方法,那么el-form是如何知道自己有哪些el-form-item子组件的呢
原来el-form在created时,会注册两个事件来维护一个子组件数组,el-form-item在mounted和beforeDestroy时会分别触发el-form的这两个事件,由此一来el-form就能在挂载完成时获取到所有el-form-item组件了
表单验证功能总结
组件加载阶段
- el-form在created时绑定了 el.form.addField 和 el.form.removeField 事件,用来维护el-form-item子组件实例数组 this.fields
- el-form的validate、resetFields、clearValidate方法 都是遍历调用 el-form-item子组件实例自己对应的validate、resetField、clearValidate方法
- el-form-item在mounted时通过this.dispatch(从祖先组件中找到第一个el-form实例) 派发 el.form.addField 事件,在beforeDestroy时派发el.form.removeField 事件
- el-form-item在mounted时,会给自己绑定上 el.form.blur 和 el.form.change 事件,事件的回调函数是调用自己的validate方法:根据自己绑定的 表单字段prop 通过 第三方校验库 async-validator执行校验
- el-form-item通过computed从祖先组件中找出el-form组件实例,获取到el-form的表单数据对象model 和 表单规则配置对象rules
- 表单组件通过 this.dispatch(从祖先组件中找到第一个el-form-item实例) 自定义派发给 el.form.blur 和 el.form.change 事件的时机
用户交互阶段
- 在表单组件中填数据时,达到了el-form-item触发时机【被动校验】,执行对单表单字段的校验
- 用户直接点表单的提交按钮,触发el-form的全表单校验【主动校验】,执行对全表单的校验
表单验证扩展trigger自定义事件类型