在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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容