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

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

一、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中如何优雅的实现参数验证)