美文网首页
koa中使用joi进行参数校验

koa中使用joi进行参数校验

作者: 超人鸭 | 来源:发表于2021-07-06 18:23 被阅读0次

    在编写api的时候通常都需要对参数进行校验,包括参数的类型、必填等;如果是字符串,是否可以为空、该符合什么规则等;如果是数字,最大值最小值是什么等等等等。
    在koa中我推荐使用 joi 这个库来进行参数校验。joi文档

    安装: npm install joi --save
    引入: import Joi from 'joi'

    下面来看看joi的用法:

    基础使用:

    使用joi进行校验,首先要定义它的校验规则,也叫schema

    const schema = Joi.string()
    

    上面就定义了一个校验字符串类型的规则,这个schema会有一个 validate方法,传入需要校验的值:

    const result = schema.validate('1')
    console.log(result)
    // 此时result为 { value: '1' }
    

    validate方法会返回一个对象,如果验证通过,就只会返回value属性,如果验证错误,就还有一个error对象,其中error对象的message描述了失败原因:

    const schema = Joi.string()
    const result = schema.validate(1)
    console.log(result)
    // result:
    {
      value: 1,
      error: [Error [ValidationError]: "value" must be a string] {
        _original: 1,
        details: [ [Object] ]
      }
    }
    
    console.log(result.error.message)
    // "value" must be a string
    

    验证对象

    既然是对koa的接口进行参数校验,无论是ctx.request.query还是ctx.request.body都是对象,那么使用joi校验基本都是对象校验了。

    与上面验证string一样,都是定义一个schema:

    const schema = Joi.object({
      name: Joi.string().allow('').required(),
      age: Joi.number().min(18).max(35),
      skill: Joi.array().items(Joi.string()).length(3)
    })
    
    const { error } = schema.validate({
      name: 'chaorenya',
      age: 20,
      skill: ['vue', 'koa', 'react']
    })
    console.log(error) // undefined
    

    其实就是在object中组合嵌套其他规则,顺便解释下上面几个规则:

    name: Joi.string().allow('').required()
    代表着name属性为必填,而且可以支持空字符串,Joi.string()默认情况不支持空字符串。

    age: Joi.number().min(18).max(35)
    代表age属性需要是一个数字,且最小为18最大为35

    skill: Joi.array().items(Joi.string()).length(3)
    代表skill属性为一个数组且数组长度为3且都得是字符串

    其中age和skill没有设置required,可以不填,但是填了的话就必须按照上面的规则。
    而且这个对象不能存在其他属性,如果需要允许其他属性的出现,需要在跟上一个unknown方法:

    const { error } = schema.validate({
      name: 'chaorenya',
      age: 20,
      skill: ['vue', 'koa', 'react'],
      other: 'other'
    }).unknown() // 允许出现其他字段
    

    多个属性校验结合

    有些情况某个字段的校验规则是根据另一个字段来规定的。

    in:

    const schema = Joi.object({
      a: Joi.array().items(Joi.number().valid(18,35)),
      b: Joi.number().valid(Joi.in('a'))
    })
    const { error } = schema.validate({
      a: [18, 35, 35],
      b: 19
    })
    console.log(error.message) // "b" must be [ref:a]
    

    a属性表示是个数组,且为number类型,并且只允许18、35这两个值当中二选一,也就是上面的valid方法。
    b属性表示必须为a属性其中的一项。

    ref:

    const schema = Joi.object({
      a: Joi.string().pattern(/^[1-9][0-9]*$/).required(),
      b: Joi.object({
        c: Joi.any().required(),
        d: Joi.ref('c'),
        e: Joi.ref('c', { ancestor: 1 }),
        f: Joi.ref('a', { ancestor: 2 })
      })
    })
    const { error } = schema.validate({
      a: '10',
      b: {
        c: 10,
        d: 10,
        e: 10,
        f: '10'
      }
    })
    console.log(error) // undefined
    

    使用ref可以指向其他属性,需要填写上属性名,然后可以指定在哪一层对象中寻找指向的属性,也就是上面的{ ancestor: 1 },不写的情况也就是上面例子中的b.c,会在当前对象中寻找,等同于写上{ ancestor: 1 }{ ancestor: 2 }代表在上一层对象中寻找,如果找不到就会验证失败。
    所以上面的例子就是a属性为/^[1-9][0-9]*$/正则校验通过的字符串,b为对象,b.c可以为任何类型,b.d要求与b.c一致,b.e也要求与b.c一致,b.f要求与外面的a属性一致。

    with、without、xor:

    with:
    const schema = Joi.object({
      a: Joi.any(),
      b: Joi.any()
    }).with('a', 'b');
    const { error } = schema.validate({
      a: 1
    })
    console.log(error.message) // "a" missing required peer "b"
    

    使用with表示当设置的属性有一个出现了,其他也必须出现,上面的例子设置了a、b属性,需要同时存在或者同时不存在。

    without:
    const schema = Joi.object({
      a: Joi.any(),
      b: Joi.any()
    }).without('a', ['b']);
    const { error } = schema.validate({
      a: 1,
      b: 1
    })
    console.log(error.message)  // "a" conflict with forbidden peer "b"
    

    without第一个参数设置条件字段,第二个参数为字段数组,表示第一个参数字段存在的话,第二个参数数组里面的字段都不能存在。上面的例子就是当a字段出现时,b字段就不能存在。

    xor:
    const schema = Joi.object({
      a: Joi.any(),
      b: Joi.any()
    }).xor('a', 'b');
    const { error } = schema.validate({
    })
    console.log(error.message) // "value" must contain at least one of [a, b]
    

    xor表示设置的参数需要任何一个或多个存在。

    when:

    const schema = Joi.object({
      mode: Joi.string().allow('email', 'phone').required(),
      address: Joi.string().when('mode', { is: 'email', then: Joi.string().email() }).required()
    });
    const { error } = schema.validate({
      mode: 'email',
      address: '11111'
    })
    console.log(error.message)  // "address" must be a valid email
    
    const { error } = schema.validate({
      mode: 'phone',
      address: '11111'
    })
    console.log(error) // undefined
    

    when相当于条件判断,第一个参数传递属性名,is相当于ifthen后面就是is为真的时候的校验条件。
    所以上面的例子就是mode字段只允许传入'email'和'phone',当mode字段为'email'的时候,address字段就会进行email校验(joi自带的字符串邮箱校验)。

    封装中间件

    先看看文件结构:


    image.png

    既然是接口校验,那么校验的文件跟路由文件对应,每个中间件单独一个文件。

    先看validator下的user文件:

    import Joi from 'joi'
    
    export const addUserSchema = Joi.object({
      userName: Joi.string().alphanum().required(),
      password: Joi.string().pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/).required(),
      age: Joi.number().min(18).max(35).required(),
      createTime: Joi.date().timestamp('javascript').max('now').required()
    })
    

    定义了一个添加用户的接口参数校验,其中的createTime代表着毫秒时间戳,不能超过当前时间。

    接下来看一下middlewares下的validateParams中间件:

    /**
     * @description 参数校验中间件
     */
    
     import { RouterCtx, middleNext } from '../utils/types'
     import Joi from 'joi'
     import { ErrorModel } from '../utils/ResModel'
     import { paramsErrorInfo } from '../utils/ErrorInfo'
     
     function genValidateParams (method: string, schema: Joi.Schema) {
       async function validateParams (ctx: RouterCtx, next: middleNext) {
         let data: any
         if (method === 'get') {
           data = ctx.request.query
         } else {
           data = ctx.request.body
         }
         const { error } = schema.validate(data)
         if (error) {
           ctx.body = new ErrorModel({ // 返回错误信息,这里为其他地方封装,不用理会
             ...paramsErrorInfo,
             message: error.message || paramsErrorInfo.message
           })
           return
         }
         await next()
       }
       return validateParams
     }
     
     export default genValidateParams
    

    因为koa的中间件参数固定为ctx与next,所以这里设计成一个工厂模式,可以将具体的schema传递进来。约定get方法参数传递在ctx.request.query,其他方法参数传递在ctx.request.body,对参数对象进行校验。

    最后看一下路由:

    import Router from 'koa-router'
    const router = new Router({
      prefix: '/user'
    })
    
    import validateParams  from '../middlewares/validateParams'
    import { addUserSchema } from '../validator/user'
    
    router.post('/add', validateParams('post', addUserSchema), async (ctx, next) => {
      ctx.body = ctx.request.body
    })
    
    router.allowedMethods()
    
    export default router
    

    在进入主逻辑之前先走一遍参数校验,传递校验规则schema与method。
    下面用postman模拟一下这个接口请求:

    校验成功:


    image.png

    校验失败:


    image.png

    用joi这个插件来做参数校验还是非常nice的,超人鸭之前使用过ajv,但是文档比较难看懂,所以这次尝试了joi。

    我是鸭子,祝你幸福。

    相关文章

      网友评论

          本文标题:koa中使用joi进行参数校验

          本文链接:https://www.haomeiwen.com/subject/rvygultx.html