边写边学系列(二) —— 使用express-validator进行后端校验

边写边学系列目录

【一】:使用apidoc,搞定自动化文档前端

【二】:使用express-validator进行后端校验node

这个系列文章主旨就是经过写代码来入门,并不深刻。只是记录我平时使用到了什么新的技术或插件的入门过程~react

express-validator

最近写node端后台写的比较多,慢慢的发现前端呢转node端虽然没有那么难,可是有不少细节的东西尚未掌握,好比之前先后端分离的时候,对于一些需求模糊的表单场景,前端可能约束会很松,大部分的约束都是后端去作的,而后用户提交的信息某个字段不合法也是后端反馈给咱们异常,前端再作处理。git

由于后端直接接触的就是数据库,每个字段都必须严格约束,因此对于接口字段的验证,特别是post(往数据库insert)的时候,验证必须严格,咱们总不能每个接口都本身写一套正则来进行校验吧,想想也是,express庞大的社区确定已经有相似的中间件了。去npm搜了一下关键字express + validate。映入眼帘的就是这个 —— express-validator。github

// express-validator官网描述是一个基于validator.js封装的express中间件。
express-validator is a set of express.js middlewares that wraps validator.js validator and sanitizer functions.
复制代码

Getting Started

仍是沿用第一节的套路,无论你三七二十一,先按照官网示例,跑通一个Demo,而后我在慢慢来弄~ 这里我依然节省时间,直接使用我以前写过的全栈脚手架express-react-scaffold来直接使用express-validator数据库

关于这个脚手架的文章在这里新手搭建简洁的Node+React脚手架,正好也是个人第一篇文章,有不少小伙伴也点过赞,一直没时间维护,借此机会温故知新一下,简单回顾了一下,发现当时写的真心锉啊,借此机会小改一下吧~express

其实对于后端接口字段校验,首先想到的就是表单提交了,由于对于GET请求,不管是query仍是param,大部分校验工做前端来作就已经能够解决问题了,query和param的合法性经过了,通常后端也就不出问题了(固然,并非说后端就没必要校验了)。而post、put等这种涉及到操做数据库的请求,若是字段类型不匹配,就很容易发生未知错误,并且由于表单里不一样表单项会有繁琐的校验规则,因此后端必须控制好~npm

以注册接口为例,跑第一个成功Demo

咱们先来看一下之前的注册接口:json

能够看到,须要三个字段,那么咱们假设是这样的:

前端:
    用户名:非空
    邮箱:必须是邮箱类型
    密码:非空
后端:
    用户名:必须大于6位
    邮箱:必须是邮箱类型
    密码:必须大于6位
复制代码

从上面咱们能够看出,先后端约束条件不一样,也就是说可能存在前端输入合法然后端输入不合法的场景~后端

从文档的例子咱们能够知道,express-validator的校验只须要在路由path和handler中间插入校验规则数组,咱们来写一下。

// 原来的接口
// 用户注册接口
router.post('/register', (req, res) => {
    User.findOne({ //查找是否存在
      username: req.body.username,
    },(err, user)=>{
        if (err) {
            res.send('server or db error');
        } else {
            if (user === null) {
                const insertObj = {
                  username: req.body.username,
                  password: md5(req.body.password + MD5_SUFFIX),
                  email: req.body.email,
                  role: 10.0
                };
                const newUser = new User(insertObj);
                newUser.save(insertObj, (err, doc) => {
                    if (err) {
                        res.json({ result: false, msg: '用户注册失败' });
                    } else {
                        console.log(doc);
                        res.json({ result: true, msg: '用户注册成功' });
                    }
                });
            } else {
                res.json({ result: false, msg: '用户名已存在'});
            }
        }
    });
});
复制代码
// 增长验证事后的接口
// 用户注册接口
router.post('/register', [
  check('username').isLength({ min: 6 }),
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
], (req, res) => {
  // Finds the validation errors in this request and wraps them in an object with handy functions
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(422).json({ errors: errors.array() });
  }
    User.findOne({ //查找是否存在
      username: req.body.username,
    },(err, user)=>{
        if (err) {
            res.send('server or db error');
        } else {
            if (user === null) {
                const insertObj = {
                  username: req.body.username,
                  password: md5(req.body.password + MD5_SUFFIX),
                  email: req.body.email,
                  role: 10.0
                };
                const newUser = new User(insertObj);
                newUser.save(insertObj, (err, doc) => {
                    if (err) {
                        res.json({ result: false, msg: '用户注册失败' });
                    } else {
                        console.log(doc);
                        res.json({ result: true, msg: '用户注册成功' });
                    }
                });
            } else {
                res.json({ result: false, msg: '用户名已存在'});
            }
        }
    });
});
复制代码

好,而后咱们来试一下:

测试用例: 用户名 - aaa, 用户邮箱 - aaa@126.com, 密码 - aaa
复制代码

如图,能够看到,前端经过以后,后端没经过,说明咱们写的内容生效了。因此!咱们的第一个validate demo也就写完了。

知其然也知其因此然

上面第一个例子虽然生效了,可是我其实仍是有点稀里糊涂,相信小伙伴们也同样,凭啥?为啥就那么加就经过了?别急,咱们一步一步来。 先来看看代码:

// 校验内容部分
[
  check('username').isLength({ min: 6 }),
  check('email').isEmail(),
  check('password').isLength({ min: 6 })
]

// 校验结果部分
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
复制代码

校验内容部分就很简单了,无非就是约束条件,如今很简单,之后肯能会变得很复杂,可是不是要考虑的。而后就来看结果部分了,能够看到,经过validationResult(req)获取校验结果,咱们将它打印出来看一看:

{
    isEmpty: [Function],
    array: [Function],
    mapped: [Function],
    formatWith: [Function],
    throw: [Function] 
}
复制代码

能够看到校验结果返回了几个api,咱们来猜一猜或者打印一下就知道了,由于代码里只用到了isEmpty()和array(),并且意思很明显,就是若是errors.isEmpty()为真,就表示校验经过,因此用脑壳想想isEmpty()应该是bool类型返回校验是否经过,若是为假就是校验不经过,而后把不经过的数组信息返回给咱们。咱们就打印一下两者:

// isEmpty
errors.isEmpty() ====> false // 返回的是bool值,表示结果
errors.array()
[ 
    { 
        location: 'body',
        param: 'username',
        value: 'aaa',
        msg: 'Invalid value, }, { location: 'body', param: 'password', value: 'aaa', msg: 'Invalid value' } ] 复制代码

能够看到,errors.array()返回的是校验不经过的字段的数组以及对应的信息。因此关于总体的校验流程基本掌握了。接下来就是巩固加深提升的过程了~

学习使用express-validator的各类API

上面基本了解了如何在后端使用express-validator,可是有一些点仍是不理解:

好比:在handler前面加上校验数组,数组的内容是咱们写的字段,那么字段若是写错呢?
再好比:他怎么知道我想校验的字段在哪?是query仍是param仍是body仍是header呢?
复制代码

带着疑惑,咱们在看读文档,等一下,读文档以前,其实咱们能够再看看上面的错误数组:

[ 
    { 
        location: 'body',
        param: 'username',
        value: 'aaa',
        msg: 'Invalid value, }, { location: 'body', param: 'password', value: 'aaa', msg: 'Invalid value' } ] 复制代码

嗯,很明显,错误数组对于咱们的字段判断是正确的,location字段它定位的是body,确实,咱们的post接口确实将数据放到了body里。所以,应该是express-validator会check全部与咱们规定值相匹配的req字段吧,带着疑问去查阅一下文档~

还真是,咱们的check还就是把能匹配的都匹配一下,那么问题又来了,这么是否是效率会很低,既然是咱们本身写的,咱们确定知道在哪里去找,能提高效率啊~好吧,我都想到了,人家做者能想不到吗?

check API

  • 限定范围类(check, body, query, header, param, cookie)

check API就是校验各类规则的api,其中包括各类封装好的校验函数,如:isString()、isInt()、isLength({})等,除此以外还有不少限定范围的api,如图

可见,也就是上述咱们说的问题,咱们能够经过约定检索范围提高效率,好比register的接口,咱们只须要检验body的字段就好了,那么就可使用body来进行check,咱们来试一试:

const { body, validationResult } = require('express-validator/check');
[
  body('username').isLength({ min: 6 }),
  body('email').isEmail(),
  body('password').isLength({ min: 6 })
]
复制代码

换完事后,结果依然成立,其余相似的check API也相似了,就是你校验的字段在哪里就用对应API去检验就行了,提高效率~。

  • 自定义限定范围(buildCheckFunction)

出了上述限定范围,咱们还能够经过buildCheckFunction来自定义范围,好比咱们校验某个字段id只有在body或query才有效,而且是UUID类型的数据,代码以下:

const { buildCheckFunction } = require('express-validator/check');
const checkBodyAndQuery = buildCheckFunction(['body', 'query']);

app.put('/update-product', [
  // id must be either in req.body or req.query, and must be an UUID
  checkBodyAndQuery('id').isUUID()
], productUpdateHandler)
复制代码
  • 校验结果validationResult(req) 这个很简单了,就是把express req传进去,而后返回咱们上面提到的那个error对象~这个就很少作介绍了,由于官方也没有详细介绍。

  • oneOf(validationChains[, message])

这个也很简单,就是只要几个条件之中的一个知足,咱们就认为校验是经过的~这个场景说实话我还确实没想过哪里能用到,不过仍是试一试,咱们在登陆接口尝试,将用户名验证是不是字符串,密码验证变成是不是数组,这确定是个假命题,不过最后结果不出意外是经过,由于用户名是正确的:

// router login - 登陆接口
oneOf([
    body('username').isString(),
    body('password').isArray()
])

最后打印出来的结果:validationResult(req).isEmpty() === true复制代码

Validation Result API

验证结果的API,也算是最重要的API了,由于校验经过不经过,要返回给客户端什么信息,都是经过这个API获取的。

validationResult(req)

  • isEmpty()

    这个上面说过了,就是返回一个bool值,表示check部分是否有错,有错就是false,没错就是true。通常使用就是:

    if (!validationResult(req).isEmpty()) {
        res.status(错误码).json({
           错误信息 
        });
    }
    复制代码
  • formatWith(formatter) 这个api意义我我的以为也不是很大,不过算是锦上添花吧。就是能够自定义错误信息格式。

    app.post('/create-user', yourValidationChains, (req, res, next) => {
    const errorFormatter = ({ location, msg, param, value, nestedErrors }) => {
        // 定义返回错误的样式,存入array数组
        return `${location}[${param}]: ${msg}`;
      };
      const result = validationResult(req).formatWith(errorFormatter);
      if (!result.isEmpty()) {
        // { errors: [ "body[password]: must be at least 10 chars long" ] }
        return res.json({ errors: result.array() });
      }
      ...
    });
    
    复制代码
  • array([options])

    存放返回的错误信息,参数能够设置是否只返回全部错误的第一条,默认返回全部错误。

    Default : { onlyFirstError: false },若是想要默认返回第一条,设置该参数为true便可

  • mapped()

    这个API跟isArray()基本一致,就是返回错误,不过isArray()mapped()的区别就是一个返回的是数组,一个返回的是对象,mapped()返回的是key和value键值对,value跟array数组返回的内容一致。

    // 假设我把login接口的username和password的check验证都改为isArray()。
     [
        body('username').isArray(), 
        body('password').isArray()
     ],
     
    // validationResult(req).mapped()
    { 
        username: { 
          location: 'body',
          param: 'username',
          value: 'luffy',
          msg: 'Invalid value' 
        },
       password: { 
          location: 'body',
          param: 'password',
          value: '123456',
          msg: 'Invalid value' 
        } 
    }
    复制代码
  • throw()

    使用这个api就是不使用isEmpty(),经过throw()一个error来返回错误。

    try {
      validationResult(req).throw();
      // Oh look at ma' success! All validations passed! } catch (err) { console.log(err.mapped()); // Oh noes! } 复制代码

filter API + Validation Chain API

其实上面两个API我以为已经足够了,基本知足业务场景了,不过express-validator还提供不少更完善的功能。下面这些API就简单过一下吧,若是有我以为能用获得的,就写个例子,我以为check就够用了~哈哈。

  • sanitize系列

    这个与check API相似,也能够限定范围和自定义范围,用处与check API不同,check API是检验对应参数是否合法,sanitize系列API是能够帮咱们提早作一个转换工做,好比咱们后台要求的是数字1,可是前端传过来的是字符串'1',就能够经过sanitize系列API进行转换。

    const { buildSanitizeFunction } = require('express-validator/filter');
    const sanitizeBodyAndQuery = buildSanitizeFunction(['body', 'query']);
    
    app.put('/update-product', [
      // 限定范围在body和query内,将id转换成整型
      sanitizeBodyAndQuery('id').toInt()
    ], productUpdateHandler)
    复制代码
  • validation chain

    这个也不算新的API,应该就是特性吧,感受跟jQuery同样,不断的链式调用,每一次调用都返回新的结果~

    // 检验weekday字段是否不在['sunday', 'saturday']内。
    check('weekday').not().isIn(['sunday', 'saturday'])
    复制代码
  • withMessage

    这个不是新API,官方是列在Validation Chain API里的,不过我以为这个是个颇有用的API,全部就单独拿出来讲一下,就是错误消息能够自定义,咱们能够设置返回消息放在里面。

    // 验证部分
    [
        body('username').isArray().withMessage('username类型不正确'), 
        body('password').isArray().withMessage('password类型不正确')
     ],
    // 结果部分
    { 
        username: { 
          location: 'body',
          param: 'username',
          value: 'luffy',
          msg: 'username类型不正确' 
        },
       password: { 
          location: 'body',
          param: 'password',
          value: '123456',
          msg: 'password类型不正确' 
        } 
    }
    复制代码

结尾

这篇文章,一如既往,仍是个人我的学习过程,若是有人没用过或者想要在本身的项目中使用,应该仍是个不错的教程~

Demo代码地址

相关文章
相关标签/搜索