Joi JS Joi — Node.js 和 Express 的出色代码验证
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
数据验证是一个有趣的话题,我们往往会写出看起来很糟糕的代码,因为它包含大量的检查。在不同的场景下,我们需要执行这些检查,例如验证来自后端端点的响应,或者验证 REST API 中的内容不会破坏我们的代码。我们将重点关注后者,即如何验证我们的 API。
考虑一下当我们没有验证库时可能需要编写的以下代码:
if (!data.parameterX) {
throw new Exception('parameterX missing')
}
try {
let value = parseInt(data.parameterX);
} catch (err) {
throw new Exception('parameterX should be number');
}
if(!/[a-z]/.test(data.parameterY)) {
throw new Exception('parameterY should be lower caps text')
}
我想你从上面这段令人尴尬的代码中已经明白了。我们倾向于对参数进行大量测试,以确保它们是正确的,并且/或者它们的值包含在允许的范围内。
作为开发人员,我们往往对这样的代码感到非常糟糕,所以我们要么开始为此编写一个库,要么转向我们的老朋友 NPM,并希望其他开发人员感受到这种痛苦,并且有太多的空闲时间,并制作一个您可以使用的库。
有很多库可以帮你实现这一点。我打算描述一个叫 Joi 的库。
在本文中,我们将一起经历以下旅程:
- 看看Joi 的功能
- 了解如何在请求管道的后端使用 Joi
- 通过在 Node.js 中为 Express 构建中间件来进一步改进
Joi 简介
安装 Joi 非常简单。我们只需要输入:
npm install joi
之后,我们就可以使用它了。让我们快速看一下如何使用它。我们要做的第一件事是导入它,然后设置一些规则,如下所示:
const Joi = require('joi');
const schema = Joi.object().keys({
name: Joi.string().alphanum().min(3).max(30).required(),
birthyear: Joi.number().integer().min(1970).max(2013),
});
const dataToValidate = {
name 'chris',
birthyear: 1971
}
const result = Joi.validate(dataToValidate, schema);
// result.error == null means valid
上面我们看到的是我们正在做的事情:
- 构建一个模式,我们调用 Joi.object(),
- 验证我们的数据,我们的调用
Joi.validate()
和dataToValidate
模式作为输入参数
好了,现在我们了解了基本动作。我们还能做什么呢?
嗯,Joi 支持各种原语以及正则表达式,并且可以嵌套到任意深度。让我们列出它支持的一些不同结构:
- string,这表示它必须是字符串类型,我们像这样使用它
Joi.string()
- number、Joi.number() 以及支持 min() 和 max() 等辅助操作,像这样
Joi.number().min(1).max(10)
- required,我们可以借助 required 方法判断某个属性是否是必需的,如下所示
Joi.string().required()
- any,这意味着它可以是任何类型,通常,我们倾向于将它与辅助函数 allow() 一起使用,以指定它可以包含的内容,如下所示,
Joi.any().allow('a')
- 可选的,严格来说,它不是一种类型,但它有一个有趣的效果。例如,如果你指定 prop:
Joi.string().optional
。如果我们不提供 prop,那么每个人都会满意。但是,如果我们提供了它并将其设置为整数,验证就会失败。 - 数组,我们可以检查该属性是否是一个字符串数组,那么它看起来就像这样
Joi.array().items(Joi.string().valid('a', 'b')
- regex,它也支持使用 RegEx 进行模式匹配,如下所示
Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/)
Joi 的 API 非常庞大。我建议你看一下是否有一个辅助函数可以解决你遇到的我上面没有展示的情况。
Joi API
嵌套类型
好的,到目前为止,我们只展示了如何声明一个一级深度的模式。我们通过调用以下命令来实现:
Joi.object().keys({ });
这说明我们的数据是一个对象。然后我们向对象添加一些属性,如下所示:
Joi.object().keys({
name: Joi.string().alphanum().min(3).max(30).required(),
birthyear: Joi.number().integer().min(1970).max(2013)
});
现在,嵌套结构其实大同小异。让我们创建一个全新的架构,一个博客文章的架构,如下所示:
const blogPostSchema = Joi.object().keys({
title: Joi.string().alphanum().min(3).max(30).required(),
description: Joi.string(),
comments: Joi.array().items(Joi.object.keys({
description: Joi.string(),
author: Joi.string().required(),
grade: Joi.number().min(1).max(5)
}))
});
特别注意这个comments
属性,它看起来和我们第一次进行的外部调用一模一样,而且完全一样。嵌套就是这么简单。
Node.js Express 和 Joi
像这样的库很棒,但如果我们能以更无缝的方式使用它们,比如在请求管道中,岂不是更好?首先让我们看看如何在 Node.js 的 Express 应用中使用 Joi:
const Joi = require('joi');
app.post('/blog', async (req, res, next) => {
const { body } = req; const
blogSchema = Joi.object().keys({
title: Joi.string().required
description: Joi.string().required(),
authorId: Joi.number().required()
});
const result = Joi.validate(body, blogShema);
const { value, error } = result;
const valid = error == null;
if (!valid) {
res.status(422).json({
message: 'Invalid request',
data: body
})
} else {
const createdPost = await api.createPost(data);
res.json({ message: 'Resource created', data: createdPost })
}
});
上面的方法可行。但对于每条路线,我们必须:
- 创建一个模式
- 称呼
validate()
找不到更好的词来形容它,就是缺乏优雅。我们想要一些看起来光鲜亮丽的东西。
构建中间件
我们看看能不能把它稍微改造成一个中间件。Express 中的中间件只是一些我们可以在需要时插入请求管道的东西。在我们的例子中,我们希望尝试验证我们的请求,并尽早确定是否值得继续执行或中止它。
那么我们来看一下中间件。它只是一个函数:
const handler = (req, res, next) = { // handle our request }
const middleware = (req, res, next) => { // to be defined }
app.post( '/blog', middleware, handler )
如果我们可以为中间件提供一个模式那就太好了,所以我们在中间件函数中要做的就是这样的:
(req, res, next) => {
const result = Joi.validate(schema, data)
}
我们可以创建一个包含工厂函数的模块,并用于所有模式。我们先来看看工厂函数模块:
const Joi = require('joi');
const middleware = (schema, property) => {
return (req, res, next) => {
const { error } = Joi.validate(req.body, schema);
const valid = error == null;
if (valid) {
next();
} else {
const { details } = error;
const message = details.map(i => i.message).join(',');
console.log("error", message);
res.status(422).json({ error: message }) }
}
}
module.exports = middleware;
接下来让我们为所有模式创建一个模块,如下所示:
// schemas.js
const Joi = require('joi')
const schemas = {
blogPOST: Joi.object().keys({
title: Joi.string().required
description: Joi.string().required()
})
// define all the other schemas below
};
module.exports = schemas;
好的,让我们回到我们的应用程序文件:
// app.js
const express = require('express')
const cors = require('cors');
const app = express()
const port = 3000
const schemas = require('./schemas');
const middleware = require('./middleware');
var bodyParser = require("body-parser");
app.use(cors());
app.use(bodyParser.json());
app.get('/', (req, res) => res.send('Hello World!'))
app.post('/blog', middleware(schemas.blogPOST) , (req, res) => {
console.log('/update');
res.json(req.body);
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
测试一下
有很多方法可以测试这一点。我们可以fetch()
从浏览器控制台进行调用,或者使用 cURL 等等。我们选择使用一个名为 的 Chrome 插件Advanced REST Client
。
我们尝试向 发出一个 POST 请求/blog
。记住,这条路由的 schema 规定 title 和 description 是必填项,所以让我们尝试让它崩溃,省略 title 看看会发生什么:
啊哈,我们收到了422
状态码,而且消息标题是必填的,所以 Joi 做了它该做的事情。为了安全起见,我们重新添加一下标题:
好的,快乐的一天,它又可以工作了。
支持路由器和查询参数
好的,太好了,我们可以处理 POST 请求中的 BODY,那么路由器参数和查询参数呢?我们想用它们来验证什么:
- 查询参数,这里有必要检查诸如 page 和 pageSize 之类的参数是否存在,并且是否为数字类型。想象一下,我们发出一个疯狂的请求,而我们的数据库包含数百万个产品,哎呀 :)
- 路由器参数,在这里首先检查我们是否收到一个数字,如果我们应该收到一个数字(例如,我们可以发送 GUID),然后检查我们没有发送明显错误的东西,比如 0 之类的
添加查询参数支持
好的,我们知道 Express 中的查询参数位于 下request.query
。因此,我们在这里可以做的最简单的事情就是确保我们的middleware.js
接受另一个参数,如下所示:
const middleware = (schema, property) => { }
因此,我们的完整代码middleware.js
如下所示:
const Joi = require('joi');
const middleware = (schema, property) => {
return (req, res, next) => {
const { error } = Joi.validate(req[property], schema);
const valid = error == null;
if (valid) { next(); }
else {
const { details } = error;
const message = details.map(i => i.message).join(',')
console.log("error", message);
res.status(422).json({ error: message })
}
}
}
module.exports = middleware;
这意味着我们必须重新审视app.js
并修改middleware()
函数的调用方式。首先,我们的 POST 请求现在应该像这样:
app.post(
'/blog',
middleware(schemas.blogPOST, 'body') ,
(req, res) => {
console.log('/update');
res.json(req.body);
});
正如您所看到的,我们在调用中添加了另一个参数主体middleware()
。
现在让我们添加我们感兴趣的查询参数的请求:
app.get(
'/products',
middleware(schemas.blogLIST, 'query'),
(req, res) => { console.log('/products');
const { page, pageSize } = req.query;
res.json(req.query);
});
如你所见,上面我们要做的就是添加参数查询。最后,让我们看看我们的schemas.js
:
// schemas.js
const Joi = require('joi');
const schemas = {
blogPOST: Joi.object().keys({
title: Joi.string().required(),
description: Joi.string().required(),
year: Joi.number() }),
blogLIST: {
page: Joi.number().required(),
pageSize: Joi.number().required()
}
};
module.exports = schemas;
正如您在上面看到的,我们已经添加了blogLIST
条目。
测试一下
让我们回到高级 REST 客户端,看看如果我们尝试在/products
不添加查询参数的情况下导航会发生什么:
如你所见,Joi 启动并告诉我们page
缺少了 。
让我们确保page
和pageSize
已添加到我们的 URL 中,然后重试:
好的,大家又开心了。:)
添加路由器参数支持
就像查询参数一样,我们只需要指出参数的位置即可。在 Express 中,这些参数位于 下req.params
。感谢我们已经完成的工作,middleware.js
我们只需要app.js
使用新的路由条目更新 ,如下所示:
// app.js
app.get(
'/products/:id',
middleware(schemas.blogDETAIL, 'params'),
(req, res) => {
console.log("/products/:id");
const { id } = req.params;
res.json(req.params);
}
)
此时我们需要进入schemas.js
并添加blogDetail
条目,因此schemas.js
现在应该如下所示:
// schemas.js
const Joi = require('joi');
const schemas = {
blogPOST: Joi.object().keys({
title: Joi.string().required(),
description: Joi.string().required(),
year: Joi.number() }),
blogLIST: {
page: Joi.number().required(),
pageSize: Joi.number().required()
},
blogDETAIL: {
id: Joi.number().min(1).required()
}
};
module.exports = schemas;
尝试一下
最后一步是尝试一下,所以我们先测试一下导航到/products/abc
。这应该会抛出一个错误,因为我们只接受大于 0 的数字:
好的,现在有一个 URL 说明/products/0
我们的另一个要求:
而且,正如预期的那样,它失败了。
概括
我们介绍了验证库 Joi,并介绍了一些基本功能及其使用方法。最后,我们研究了如何为 Express 创建中间件,以及如何巧妙地使用 Joi。
总而言之,我希望这篇文章能给你带来教育意义。