基于 schema 的数据校验
前端开发中,对要提交的表单数据进行校验是很常见的需求,有开源的基于框架的数据校验库,也有组件库内置的校验功能,这里介绍的是一种脱离框架、组件的独立数据校验思路。
我们团队的 Vue 项目比较多,先看下这一块的数据校验方案:
- vuelidate:https://github.com/vuelidate/vuelidate
- Element UI:https://element.eleme.io/2.8/#/zh-CN/component/form
vuelidate
vuelidate 是基于 Vue 的数据校验库,特点是根据定义的校验规则,在数据变更时自动校验,利用了 Vue 数据响应式机制:
import { required, minLength, between } from 'vuelidate/lib/validators'
export default {
data() {
return {
name: '',
age: 0
}
},
validations: {
name: {
required,
minLength: minLength(4)
},
age: {
between: between(18, 30)
}
}
}
实现要引入 vuelidate 到 Vue,从而通过 validations
声明的数据校验规则,在实例初始化后,会生成对应的 $v
数据,记录内部各项校验的结果:
$v: {
name: {
"required": false,
"minLength": false,
"$invalid": true,
"$dirty": false,
"$error": false,
"$pending": false
},
age: {
"between": false
"$invalid": true,
"$dirty": false,
"$error": false,
"$pending": false
}
}
无论是在 UI 组件上展示校验错误文案,还是在表单提交时获取校验结果,都是通过访问 $v
实现。
Element UI
作为组件库,Element UI 的数据校验与表单组件直接关联,也是先定义校验规则:
export default {
data() {
const ageValidator = (rule, value, callback) => {
if (value < 18) return callback(new Error('年龄不小于18'))
if (value > 30) return callback(new Error('年龄不大于30'))
callback()
}
return {
form: {
name: '',
age: 0
},
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 4, max: 8, message: '长度在 4 到 8 个字符', trigger: 'blur' }
],
age: [
{ validator: ageValidator, trigger: 'blur' }
]
}
}
}
看起来差不多,但是由于组件的支持,使用起来比较方便,只进行一次绑定就好:
<el-form :model="form" :rules="rules">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input v-model="form.age" type="number" />
</el-form-item>
</el-form>
用户输入错误时,组件可以直接展示错误文案,另外表单组件上还定义了 validate()
方法,可以在提交时手动调用进行数据校验。
上面介绍的两种数据校验方案,都可以满足日常表单校验需求。不过两者都有一个问题,依赖其他框架、库。这使得其应用场景受限,显然在 Node 应用中就不方便使用。也可以认为,作为数据校验方案,两者都不够是“纯粹”。
下面介绍一个比较“纯粹”的方案:
schema-typed
项目地址:https://github.com/rsuite/schema-typed
schema-typed 首先为需要校验的数据创建一个模型:
import { SchemaModel, StringType, NumberType } from 'schema-typed'
const model = SchemaModel({
name: StringType().isRequired('姓名不能为空'),
age: NumberType().range(18, 30, '年龄应在18-30之间')
})
然后使用模型来校验数据:
model.check({
name: 'foo',
age: 40
})
// 结果:
// {
// name: { hasError: false },
// age: { hasError: true, errorMessage: '年龄应在18-30之间' }
// }
好像也没啥了不起。不过既然是数据的 shema,对于复杂数据结构也是有支持的,例如:
import {
SchemaModel, StringType, NumberType, DateType, ArrayType, ObjectType
} from 'schema-typed'
const model = SchemaModel({
accountId: StringType().isRequired('账号不能为空'),
trades: ArrayType().of(
ObjectType().shape({
tradeId: StringType().isRequired('交易号不能为空'),
tradeAmount: NumberType().min(0, '交易金额不能小于0')
})
)
})
model.check({
accountId: 'foo@163.com',
trades: [
{tradeId: '001', tradeAmount: 123.45},
{tradeId: '002', tradeAmount: 0.89 }
]
})
schema-typed 原本是作者的 React 组件工具集的其中一个工具,但是显然,它可以直接应用到 Vue 项目甚至 Node 项目中。
并且,这种定义数据 schema,基于 schema 对数据校验的方式,显然对于业务代码的拆分也很有帮助。
写到这里,介绍了几种数据校验方案好像也就差不多了。不过我还要再额外介绍一下自己基于 shema-typed 改进的数据校验库:
schema-validate
项目地址:https://github.com/luobotang/schema-validate
先说下我认为 schema-typed 不太好的一些细节:
- StringType()、NumberType() 这样的名字太啰嗦了,写出来的 shema 不够简洁易读
- NumberType() 内部其实是兼容
'123'
这样的类似数值的字符串的,不太“严谨” - 每个规则的错误文案都需要单独指定,不然缺省错误文案是没法用的(还是英文的)
- 不支持一个属性字段对应多种类型的情况
- 多数规则方法名称也太啰嗦
先看结果吧,经过改造之后,之前 schema-validate 的例子变成:
import { SchemaModel, T } from '@luobotang/schema-validate'
const model = SchemaModel({
name: T.string('姓名').required(),
age: T.number('年龄').range(18, 30)
})
model.check({
name: 'foo',
age: 40
})
// 结果:
// {
// name: { hasError: false },
// age: { hasError: true, errorMessage: '年龄应在 18 到 30 之间' }
// }
怎么样,是不是稍微清爽了一些。
来看一个真实业务案例:
代码示例说明:以下示例代码不涉及业务机密,校验规则来源于公开的央行反洗钱上报数据规范。
图中的 BankName
、BankAccount
、Placeholder
、IP
都是已经定义好的校验类型,通过 .clone()
重新指定数据描述(以便在错误文案中提现数据字段信息)或直接复用。
图中的 T.any()
就是支持字段数据多类型的机制,匹配任一内部类型都视为校验通过。
此外,如果按照原来 schema-typed 的方式,通过 model.check(data)
返回的是一个复杂的对象,包含各个字段的校验结果,对于想直接获得一个验证结果的情况,就需要自己遍历结果去查找了。为此,在 schema-validate 中新增了一个 model.validate(data)
方法:
mode.validate(data)
// 结果:
// {
// hasError: true,
// errorMessage: 'xxx'
// }
并且在内部执行中,遇到第一个校验错误就会直接返回,不再执行其他字段的校验。
总结
通过 schema 的方式来“声明”数据结构,并用于数据校验,在我看来是比较“清爽”的方式。当然,相比 vuelidate 和 Element UI 来说,需要开发者做一些额外工作,但这在我看来反倒是优势。这是这些沉淀下来的业务数据的 schema,是不会随着技术栈的更新而被迫更新,并且可以在多个不同技术栈的项目中复用。
额外畅想一下,这些数据 schema,是不是也可以用于生成 mock 数据呢?