美文网首页
在koa中如何优雅的实现参数验证

在koa中如何优雅的实现参数验证

作者: 名字还是土一点好 | 来源:发表于2018-07-06 22:27 被阅读0次

    代码写久了,说不出几句文邹邹的话。。。

    一、koa-middle-validator

    express有个非常好用的中间件 express-validator,它既可以用作参数的验证,如校验 request body 、query parmas、headers等等,又支持参数的格式化。
    很遗憾的是,这么好用的库没有提供Koa的版本。于是笔者在fork此项目的基础之上,实现了对koa的支持,衍生出了 koa-middle-validator(npm类似包名太多,所以用了这个)。

    这个仓库在一年之前就已经发布过了,笔者也多次运用在实际上线项目中

    使用方法

    const util = require('util'),
    const Koa = require('koa');
    const bodyParser = require('koa-bodyparser');
    const convert = require('koa-convert');
    const koaValidator = require('koa-middle-validator');
    const Router = require('koa-router');
    const _ = require('lodash');
    
    const app = new Koa();
    const router = new Router();
    
    app.use(convert(bodyParser()));
    app.use(koaValidator({
      customValidators: {
        isArray: function(value) {
          return _.isArray(value);
        },
        isAsyncTest: function(testparam) {
          return new Promise(function(resolve, reject) {
            setTimeout(function() {
              if (testparam === '42') { return resolve(); }
              reject();
            }, 200);
          });
        }
      },
      customSanitizers: {
        toTestSanitize: function() {
          return "!!!!";
        }
      }
    })); // this line must be immediately after any of the bodyParser middlewares!
    
    router.get(/\/test(\d+)/, validation);
    router.get('/:testparam?', validation);
    router.post('/:testparam?', validation);
    app.use(router.routes())
    app.use(router.allowedMethods({
      throw: true
    }))
    
    function validation (ctx) {
      ctx.checkBody('postparam', 'Invalid postparam').notEmpty().isInt();
      //ctx.checkParams('urlparam', 'Invalid urlparam').isAlpha();
      ctx.checkQuery('getparam', 'Invalid getparam').isInt();
    
    
      ctx.sanitizeBody('postparam').toBoolean();
      //ctx.sanitizeParams('urlparam').toBoolean();
      ctx.sanitizeQuery('getparam').toBoolean();
    
      ctx.sanitize('postparam').toBoolean();
    
      return ctx.getValidationResult().then(function(result) {
        ctx.body = {
          // return something
        }
      });
    }
    
    app.listen(8888);
    

    API

    可参照 express-validator,具体请移步 koa-middle-validator

    Middleware Options

    errorFormatter
    customValidators
    customSanitizers

    const _ = require('lodash')
    const validator = require('validator')
    const koaValidator = require('koa-middle-validator')
    
    /**
     * 自定义验证
     */
    module.exports = () => koaValidator({
      errorFormatter: (param, message, value) => {
        return {
          param,
          message,
          value,
        }
      },
      customValidators: {
        isEmail: value => /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value),
        isMobile: value => /^1[3|4|5|7|8]\d{9}$/.test(value),
        isString: value => _.isString(value),
        isNumber: value => !isNaN(Number(value)),
        isObject: value => _.isObject(value),
        isJson: value => Object.prototype.toString.call(value).toLowerCase() === '[object object]',
        isArray: value => _.isArray(value),
        inArray: (param, ...args) => {
          const validatorName = args[0]
          return _.every(param, (item) => {
            switch (validatorName) {
              case 'isEmail': return /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(item)
              case 'isMobile': return /^1[3|4|5|7|8]\d{9}$/.test(item)
              case 'isString': return _.isString(item)
              case 'isNumber': return _.isNumber(item)
              case 'isObject': return _.isObject(item)
              case 'isArray': return _.isArray(item)
              case 'isBoolean':
                switch (typeof item) {
                  case 'string': return item === 'true' || item === 'false'
                  case 'boolean': return item === true || item === false
                  default: return false
                }
              default:
                return validator[validatorName].call(this, item)
            }
          })
        },
        isBoolean: (value) => {
          switch (typeof value) {
            case 'string':
              return value === 'true' || value === 'false'
            case 'boolean':
              return value === true || value === false
            default:
              return false
          }
        },
        custom: (value, callback) => {
          if (typeof value !== 'undefined') {
            return callback(value)
          }
          return false
        },
      },
    })
    

    Validation

    ctx.check()

       ctx.check('testparam', 'Error Message').notEmpty().isInt();
       ctx.check('testparam.child', 'Error Message').isInt(); // find nested params
       ctx.check(['testparam', 'child'], 'Error Message').isInt(); // find nested params
    

    ctx.assert()
    ctx.validate()
    ctx.checkBody()

    ctx.checkBody({
        host: {
          notEmpty: {
            options: [true],
            errorMessage: 'host 不能为空',
          },
          matches: {
            options: [regx.host],
            errorMessage: 'host 格式不正确',
          },
        },
        port: {
          notEmpty: {
            options: [true],
            errorMessage: 'port 不能为空',
          },
          isInt: {
            options: [{ min: 0, max: 65535 }],
            errorMessage: 'port 需为0-65535之间的整数',
          },
        },
        db: {
          notEmpty: {
            options: [true],
            errorMessage: 'db 不能为空',
          },
          isString: { errorMessage: 'db 需为字符串' },
        },
        user: {
          optional: true,
          isString: { errorMessage: 'user 需为字符串' },
        },
        pass: {
          optional: true,
          isString: { errorMessage: 'pass 需为字符串' },
        },
      })
    

    ctx.checkQuery()
    ctx.checkParams()
    ctx.checkHeaders()
    ctx.check()

    Validation result

    • 获取验证结果 ctx.validationErrors()
    • 异步结果 ctx.getValidationResult()

    Sanitizer 参数格式化

    ctx.sanitize()
    ctx.filter()
    ctx.sanitizeBody()
    ctx.sanitizeQuery()
    ctx.sanitizeParams()

    ctx.sanitizeQuery('page').toInt()
    ctx.sanitizeQuery('pageSize').toInt()
    

    ctx.sanitizeHeaders()

    二、mongoose-validation

    习惯使用 node + mongoDB 开发项目,而 mongoose 应该算是 mongoDB 生态圈里最火的操作工具。
    在后端业务场景里,绝大部分验证的参数往往就是数据库需要存储的数据,mongoose 在建模的时候,一般都会带上参数所有的校验规则,这也是程序员必须做的。
    再加上业务层做的参数验证,这也就导致部分表里的数据做了重复的验证,加大了代码开发量。
    最近在写个人项目的同时,尝试着把mongoose内置的验证器利用起来,以减少业务代码的重复。mongoose-validation 就是这么个小东西(后续还想支持express,就这么取名了)。

    使用方法

    mongoose

    const UserSchema = new mongoose.Schema({
      name: { type: String, required: true, unique: true },
      ...
    }, {
      collection: 'user',
      id: false,
    })
    module.exports =  mongoose.model('User', UserSchema)
    

    middleware

    const _ = require('lodash')
    const validator = require('validator')
    const mongooseValidation = require('mongoose-validation')
    const { mongoose } = require('../lib/mongodb.lib')
    
    module.exports = () => mongooseValidation({
      throwError: true,
      mongoose: mongoose,
      errorFormatter: errors => {
        return {
          errors,
          code: 'VD99',
        }
      },
      customValidators: {
        isMongoId: value => validator.isMongoId(value),
        isMultiType: {
          validator: value => ['remove', 'add', 'update'].indexOf(value) !== -1,
          message: '`{ type }` must in ["remove", "add", "update"]'
        },
        isEmail: (value) => /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value),
        isMobile: value => /^1[3|4|5|7|8]\d{9}$/.test(value),
        isString: value => _.isString(value),
        isNumber: value => !isNaN(Number(value)),
        isObject: value => _.isObject(value),
        isJson: value => Object.prototype.toString.call(value).toLowerCase() === '[object object]',
        isArray: value => _.isArray(value),
        inArray: (param, ...args) => {
          const validatorName = args[0]
          return _.every(param, (item) => {
            switch (validatorName) {
              case 'isEmail': return /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(item)
              case 'isMobile': return /^1[3|4|5|7|8]\d{9}$/.test(item)
              case 'isString': return _.isString(item)
              case 'isNumber': return _.isNumber(item)
              case 'isObject': return _.isObject(item)
              case 'isArray': return _.isArray(item)
              case 'isBoolean':
                switch (typeof item) {
                  case 'string': return item === 'true' || item === 'false'
                  case 'boolean': return item === true || item === false
                  default: return false
                }
              default:
                return validator[validatorName].call(this, item)
            }
          })
        },
        isBoolean: (value) => {
          switch (typeof value) {
            case 'string':
              return value === 'true' || value === 'false'
            case 'boolean':
              return value === true || value === false
            default:
              return false
          }
        },
        custom: (value, callback) => {
          if (typeof value !== 'undefined') {
            return callback(value)
          }
          return false
        },
      },
    })
    

    controller

    try {
      await ctx.mongooseValidate({
          data: ctx.request.body,
          schema: {
            _id: { validate: 'isMongoId', required: true },
            multi: [{ type: String, validate: 'isMongoId' }],
          },
          necessary: ['type', 'multi'],
          optional: ['_id'],
        }, UserSchema)
    } catch (e) {}
    

    API

    Middleware options

    throwError
    是否抛出Error,默认 :false
    mongoose
    mongoose 对象,必须传。
    errorFormatter: function(errors){}
    错误信息格式化。默认:function (errors) { return { errors: errors } }
    customValidators
    自定义验证。与 mongoose schema 的 validate 保持一致

    Validate

    ctx.mongooseValidate(options)

    • options.data {Object}。需要校验的参数
    • options.schema {Object}。自定义校验规则
    • options.necessary {Array}。必须校验的参数
    • options.optional {Array}。没有的话,可以跳过的参数
      ctx._validationMongooseErrors
      验证的结果。没有错误默认为 []

    结语

    两个都不是什么好轮子,至少笔者觉得某种场景下还是能派上用场,所以今天整理一下放了出来,读者觉得有用可以给个Star

    相关文章

      网友评论

          本文标题:在koa中如何优雅的实现参数验证

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