基于 schema 的数据校验

前端开发中,对要提交的表单数据进行校验是很常见的需求,有开源的基于框架的数据校验库,也有组件库内置的校验功能,这里介绍的是一种脱离框架、组件的独立数据校验思路。

我们团队的 Vue 项目比较多,先看下这一块的数据校验方案:

  1. vuelidate:https://github.com/vuelidate/vuelidate
  2. 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' }
        ]
    }
  }
}

看起来差不多,但是由于组件的支持,使用起来比较方便,只进行一次绑定就好:


  
    
  
  
    
  

用户输入错误时,组件可以直接展示错误文案,另外表单组件上还定义了 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: '[email protected]',
  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 之间' }
// }

怎么样,是不是稍微清爽了一些。

来看一个真实业务案例:

说明:以下示例代码不涉及业务机密,校验规则来源于公开的央行反洗钱上报数据规范。

代码示例

图中的 BankNameBankAccountPlaceholderIP 都是已经定义好的校验类型,通过 .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 数据呢?

你可能感兴趣的:(基于 schema 的数据校验)