常见的校验方式
当用户未按要求填写表单,点击进入下一步或者提交表单的时候,给用户进行错误提示。 如下图所示:
这种方式,大家应该都是比较熟悉了。只要对需要校验的字段写好校验规则。当点击 Submit 按钮时,触发表单的全局校验就行了。
少见的校验方式
本文主题
如果是下面这种校验方式,又该怎么办呢?
只有用户按要求输入后,下一步/提交的按钮才会高亮,才能放行。
!这里允许给大家一首歌的时间去思考 🤔
背景
这篇文章的由来,也是因为遇到了这样的需求。对比了几个国内和国外的 UI 组件库之后,都找不到解决方案,也不知道怎么去搜索这种校验方式的解决方案(至今为止也不知道 😂,蠢哭)
大多数的 UI 组件库,对表单实例对象都只提供了全局校验的方法。一调用就把所有需要校验的字段都给校验了一遍,这没毛病吖。但关键是同时把错误信息给提示出来了。
当存在多个为必填的字段信息,假如从第一个开始就填错了,但一下子给用户提示了所有的错误信息。这真是不单只把用户给整不会了,把我也整不会了 😂
解决方案
想了好久好久,终于想到了一个相对较好的解决方案,但就是需要写多亿点点的代码 🙅🏻♀️
解决方案如下:
- 手动编写校验器 validator
- 收集字段的校验状态, 对每个字段校验的过程进行管控
- 每个字段触发校验后,都对所有的字段进行过滤
- 如果发现存在状态为错误的字段,则停止校验
既然要管控字段的校验过程, 那 UI 组件 Form 组件提供的内置 Rules 规则铁定是不能进行单纯地写校验规则就完事儿了。而是使用 validator 代替校验规则,即自己写一个校验器。
当遇到手写的时候,千万不要恐惧!!! 希望大家能坚持看到最后
逻辑代码
注册校验函数
实现的功能
- 收集字段状态
- 修改字段状态
- 触发校验函数
/** registerValid.ts **/
import { isFunction } from 'lodash-es'
type OptionCallback = () => void | Promise<any>
interface ValidCallback<T extends string> {
// 启动校验
launchValid: () => void
// 获取校验对象
getField: () => Record<T, boolean>
// 修改字段状态,同时启动校验,即调用 launchValid 函数
valid: (field: T, value: boolean) => void
// 创建单个字段的校验修改器
createCollector: (field: T) => [
() => void,
() => void
]
}
interface RegisterValid {
<EE extends string = string>(
initialValue: EE[] | Record<EE, boolean>,
option: { success: OptionCallback, fail: OptionCallback }
): ValidCallback<EE>
}
/**
* 注册校验函数
* @param {any} keys 需要校验的字段 | 字段初始化状态
* @param {any} option 成功校验函数和失败校验函数
* @returns {any}
*/
const registerValid: RegisterValid = (keys, option) => {
let validObj: any = keys
if (isArray(keys)) {
validObj = keys.reduce((prev, cur) => {
Reflect.set(prev, cur, false)
return prev
}, {})
}
const launchValid = () => {
let callback: any = null
const validValues = Object.values(validObj)
const result = validValues.filter(Boolean)
// 结果数组 !== 所有字段的数量
// 说明没有全部通过
callback = result.length !== validValues.length ? option.fail : option.success
callback && isFunction(callback) && callback()
}
return {
launchValid,
getField: () => validObj,
valid (field, value) {
Reflect.set(validObj, field, value)
launchValid()
},
createCollector (field) {
const errorFn = () => this.valid(field, false)
const passFn = () => this.valid(field, true)
return [errorFn, passFn]
}
}
}
export default registerValid
使用方法(假如现在要校验 name 与 post 字段)
import registerValid from '@/folder/registerValid'
// 模拟表单的校验状态
let validateStatus = false
// 假如现在需要校验账户和密码
// 1. 收集字段状态
// 初始化时 name 和 post 的字段的初始状态都为 false
const validObj = registerValid(['name', 'post'], {
success() {
// 当所有字段都通过校验后,就会触发该函数
validateStatus = true
},
fail() {
// 只要有一个字段出错,就会触发该函数
validateStatus = false
}
})
// 如果 name 拥有默认值,那么 name 的初始化状态应该为 true,而不是 false
// 在创建时可以传入一个对象
/**
const validObj = registerValid({ name: true, post: false }, {
success() {
// 当所有字段都通过校验后,就会触发该函数
validateStatus = true
},
fail() {
// 只要有一个字段出错,就会触发该函数
validateStatus = false
}
})
**/
// 2. 修改字段状态
// 修改 name 字段的状态为 true
// 内部同时会触发 validObj.launchValid 函数
validObj.valid('name', true)
// 3. 触发校验函数
// 获取当前所有字段的状态, 返回值为一个对象
const statusObj = validObj.getField()
应用到实际的表单校验中去
// 使用普通的校验规则方式
const rules = {
name: {
required: true, message: 'name 字段为必填'
},
post: {
...
}
}
// 使用校验收集的方式
const rules = {
name: { validator: nameValidator },
post: { validator: postValidator }
}
function nameValidator() {}
function postValidator() {}
自定义校验器写好了,但是一些像 required, 数据类型,最大值,最小值这些非定制化的校验规则在实际的业务开发中也手写的话,时间成本也太高了。这里第一时间想到的是使用和 UI 组件库绑定的一些规则校验器,比如说 element-ui(async-validate), arco-design(b-validate)等等等.
以 b-validator 为例,对其进行封装:
注意:这不是一个 hook, 只是实在不知道该怎么命名了
b-validate 的规则请参考它的文档
/** useBv.ts **/
import { Schema, SchemaType, SchemaRuleType } from 'b-validate/es'
/**
* 使用 bv 校验器
* @param {string} value
* @param {SchemaRuleType[]} rules
* @returns {any}
*/
const useBv = (value: string, rules: SchemaRuleType[]): Promise<string | null> => {
return new Promise((resolve) => {
const schemaRule: SchemaType = {
bv: rules
}
const schema = new Schema(schemaRule)
schema.validate({ bv: value }, (errors: any) => {
resolve(errors != null ? errors.bv.message : errors)
})
})
}
export default useBv
它所完成的事有:
- 对传入的 value,根据 rules 规则进行校验
- 以 Promise 的形式返回校验结果,如果校验通过返回的结果为 undefined
完整的一个校验 demo
import registerValid from '@/folder/registerValid'
import useBv from '@/folder/useBv'
// 表单的校验状态
const formValidateStatus = false
// 表达初始化的值
const form = {
name: '',
post: ''
}
// 校验规则
const rules = {
name: { validator: nameValidator },
post: { validator: postValidator }
}
// 创建状态收集器
const validObj = registerValid(['name', 'post'], {
success() { formValidateStatus = true },
fail() { formValidateStatus = false }
})
// 自定义 name 校验器
async function nameValidator(value: string, callback: (error?: any) => void) {
const [error, pass] = validObj.createCollector('name')
const result = await useBv(value, [
{ type: 'string', required: true, message: 'name 为必填项' }
])
if (result) {
error()
return callback(result)
}
pass()
}
// 自定义 post 校验器
function postValidator(value: string, callback: (error?: any) => void) {
const [error, pass] = validObj.createCollector('post')
const result = await useBv(value, [
{ type: 'string', required: true, message: 'post 为必填项' }
])
if (result) {
error()
return callback(result)
}
pass()
}