async-validator 前后端共用表单校验模型

前言

前后端开发中,表单校验是一个常用且必要的功能。前端开发中一些 UI 库如 ElementAnt Design 表单组件都使用了 async-validator 作为校验库。

async-validator 功能与特性:

  • 内置多种数据格式校验功能,包括数组、枚举、对象
  • 异步校验库,支持自定义校验方法
  • 一个字段支持多个校验规则,支持自定义字段错误信息
  • 可校验整个表单错误(指出每个错误的校验),也可以提前终止校验程序(一旦出错就终止校验)

中后台管理前端项目中使用了 Element 或 Ant Design 时,可在 Node.js 后端项目使用该库实现前后端同构,应用端项目亦可自行封装使用,对于同一个接口或类似接口可以使用相同的校验规则。

伪代码:

import { createProduct } from '@/model/schema'

// Webapp 自行封装验证逻辑,验证出错则提示
this.$validate(createProduct, this.record).then((error) => {
if (error) {
this.$notice(error)
return
}
// pass
})


// Element 直接使用框架自带的校验方法
// <el-form ref="record" :rules="createProduct" :model="record">
this.$refs.record.validate((valid) => { /* pass */ })


// Node.js 自行封装验证逻辑,验证出错则抛出错误
const { createProduct } = require('./model/schema')
const requestBody = await ctx.validate(createProduct)
requestBody.userId = ctx.userId
ctx.body = await ctx.db.Product.create(requestBody)

基本使用

验证规则定义:

const createProduct = {
name: { required: true, message: '请输入产品名称' }, // 必填校验
descr: { min: 3, max: 200, message: '描述长度为 3~200 位字符' }, // 长度校验
groupId: { type: 'number', required: true, message: '请选择所属分组' }, // 类型校验
// 枚举校验
role: { required: true, type: 'enum', enum: [1, 2, 3], message: '产品类型选择错误' },
// 多个规则
key: [
{
required: true,
pattern: /^[a-z_]+/,
message: '请输入合法的产品密钥',
},
{
min: 6,
max: 36,
message: '产品密钥长度为 6~36 位'
}
],
// 嵌套对象
info: {
type: 'object',
fields: {
address: { required: true },
owner: { required: true },
},
},
// 数组项校验
avatar: { type: "array" },
}

基础校验:

使用使用方式详见 async-validator 文档

import asyncValidator from 'async-validator'
import { Message } from 'element-ui'

const record = {
name: '默认产品',
descr: '测试',
role: 4,
groupId: 2,
key: '@10c61f1a1f47',
info: {
address: '浙江省杭州市',
},
avatar: ['http://static.example.com/images/10c61f1a1f47.png']
}

const validator = new schema(createProduct)

// 可配置出错即停止
validator.validate(createProduct, /* { first: true } */, (errors, fields) => {
if(errors) {
// 给出提示
Message.error(errors[0].message)
return console.log(errors)
}
console.log('校验通过')
})

示例数据校验不通过,以下为错误信息,可根据错误信息进行后续逻辑处理:

// errors
[
{
"message": "描述长度为 3~200 位字符",
"field": "descr"
},
{
"message": "请选择所属分组",
"field": "groupId"
},
{
"message": "产品类型选择错误",
"field": "role"
},
{
"message": "请输入合法的产品密钥",
"field": "key"
},
{
"message": "产品密钥长度为 6~36 位",
"field": "key"
},
{
"message": "info.address is required",
"field": "info.address"
},
{
"message": "info.owner is required",
"field": "info.owner"
},
]

// fields
{
"descr": [
{
"message": "描述长度为 3~200 位字符",
"field": "descr"
}
],
"groupId": [
{
"message": "请选择所属分组",
"field": "groupId"
}
],
"role": [
{
"message": "产品类型选择错误",
"field": "role"
}
],
"key": [
{
"message": "请输入合法的产品密钥",
"field": "key"
},
{
"message": "产品密钥长度为 6~36 位",
"field": "key"
}
],
"info.address": [
{
"message": "info.address is required",
"field": "info.address"
}
],
"info.owner": [
{
"message": "info.owner is required",
"field": "info.owner"
}
],
"avatar": [
{
"message": "avatar is not an array",
"field": "avatar"
}
]
}

Koa 中封装示例

封装目标:

  • 减少代码量,避免在 controller 层频繁引入
  • 支持排除/选取指定字段
  • 校验失败自动抛出错误,终止后续逻辑,校验成功返回处理后的数据

封装代码如下:

const asyncValidate = require('async-validator')
const Schema = require('../../lib/schema')
const { Model } = require('../db')

// schema.js
function groupExists(rule, value, cb) {
Model.group.findById(value).then((res) => {
if (res) {
return cb()
}
cb(new Error('分组不存在'))
}).catch(cb)
}

const product = {
update: {
name: { required: true, message: '请输入产品名称' },
groupId: [
{ required: true, message: '分组 ID 不能为空' },
{ validator: groupExists, } // 从数据库异步校验
],
descr: { min: 3, max: 100 },
_only: ['name', 'groupId', 'descr'], // 仅能更新这三个字段
}
}

function getSchema(schema = '') {
const [model, action] = schema.split('.')
if (model && action && Schema[model] && Schema[model][action]) {
return Schema[model][action]
}
return {}
}

/**
* 表单校验
* @param schema 待校验的模型规则或动作名称
* @param data 待校验的数据
* @param attributes 排除/选取的字段
* @returns {Promise<*>} 校验完成后的数据
*/
async validate(schema, data, attributes) {
if (!schema) {
return this.ctx.body
}
// 使用 schema.action 读取校验
if (typeof schema === 'string') {
schema = getSchema(schema)
}
// 默认使用 ctx.request.body 中的数据
if (!data) {
data = this.request.body
}
// 需要排除/选取的字段
if (!attributes) {
attributes = { only: schema._only, exclude: schema._exclude }
}
const { only, exclude } = attributes
const _data = {}
// 优先使用 only,同 mongoose
if (only) {
only.forEach(key => {
_data[key] = data[key]
})
data = _data
} else if (exclude) {
exclude.forEach(key => {
delete data[key]
})
}
// 开始验证
return new Promise(resolve => {
// { first: true } 为兼顾性能,一旦出错就终止校验后续字段
new asyncValidate(schema).validate(data, { first: true }, (errors, fields) => {
if (errors) {
this.throw(422, 'not validate', { message: errors[0].message, errors })
}
// 返回校验成功数据
resolve(data)
})
})
}



// app.js
app.context.validate = validate



// PUT /products/:id, conroller 校验通过后直接更新
const { id } = ctx.params

// 自动校验并过滤请求数据,仅更新 name, groupId, descr 三个字段
const requestBody = await ctx.validate('product.update')
ctx.body = await ctx.db.Product.findByIdAndUpdate(id, requestBody, { new: true })

至此,我们完成了 Koa 中一个相对完备的表单校验功能。

拓展阅读

使用 async-validator 编写 Form 组件