边写边学系列目录
这个系列文章主旨就是通过写代码来入门,并不深入。只是记录我平时使用到了什么新的技术或插件的入门过程~
express-validator
最近写node端后台写的比较多,慢慢的发现前端呢转node端虽然没有那么难,但是有很多细节的东西还没有掌握,比如以前前后端分离的时候,对于一些需求模糊的表单场景,前端可能约束会很松,大部分的约束都是后端去做的,然后用户提交的信息某个字段不合法也是后端反馈给我们异常,前端再做处理。
因为后端直接接触的就是数据库,每一个字段都必须严格约束,所以对于接口字段的验证,特别是post(往数据库insert)的时候,验证必须严格,我们总不能每一个接口都自己写一套正则来进行校验吧,想一想也是,express庞大的社区肯定已经有类似的中间件了。去npm搜了一下关键字express + validate
。映入眼帘的就是这个 —— express-validator。
// 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脚手架,正好也是我的第一篇文章,有很多小伙伴也点过赞,一直没时间维护,借此机会温故知新一下,简单回顾了一下,发现当时写的真心锉啊,借此机会小改一下吧~
其实对于后端接口字段校验,首先想到的就是表单提交了,因为对于GET请求,无论是query还是param,大部分校验工作前端来做就已经可以解决问题了,query和param的合法性通过了,一般后端也就不出问题了(当然,并不是说后端就不必校验了)。而post、put等这种涉及到操作数据库的请求,如果字段类型不匹配,就很容易发生未知错误,而且因为表单里不同表单项会有繁琐的校验规则,所以后端必须控制好~
以注册接口为例,跑第一个成功Demo
我们先来看一下以前的注册接口:
可以看到,需要三个字段,那么我们假设是这样的:前端:
用户名:非空
邮箱:必须是邮箱类型
密码:非空
后端:
用户名:必须大于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类型不正确' } } 复制代码
结尾
这篇文章,一如既往,还是我的个人学习过程,如果有人没用过或者想要在自己的项目中使用,应该还是个不错的教程~