智能 Node.js 表单验证
已发表: 2022-03-11在 API 中执行的一项基本任务是数据验证。 在本文中,我想向您展示如何为您的数据添加防弹验证,同时还能以良好的格式返回数据。
在 Node.js 中进行自定义数据验证既不容易也不容易。 为了涵盖任何类型的数据,您需要编写很多功能。 虽然我尝试了一些 Node.js 表单数据库(用于 Express 和 Koa),但它们从未满足我项目的需求。 扩展库和无法处理复杂数据结构或异步验证的库存在问题。
使用 Datalize 在 Node.js 中进行表单验证
这就是为什么我最终决定编写自己的小型但功能强大的表单验证库,称为 datalize。 它是可扩展的,因此您可以在任何项目中使用它并根据您的要求对其进行自定义。 它验证请求的正文、查询或参数。 它还支持async
过滤器和复杂的 JSON 结构,如数组或嵌套对象。
设置
Datalize 可以通过 npm 安装:
npm install --save datalize
要解析请求的正文,您应该使用单独的库。 如果你还没有使用过,我推荐 Koa 的 koa-body 或 Express 的 body-parser。
您可以将本教程应用于您已经设置好的 HTTP API 服务器,或使用以下简单的 Koa HTTP 服务器代码。
const Koa = require('koa'); const bodyParser = require('koa-body'); const app = new Koa(); const router = new (require('koa-router'))(); // helper for returning errors in routes app.context.error = function(code, obj) { this.status = code; this.body = obj; }; // add koa-body middleware to parse JSON and form-data body app.use(bodyParser({ enableTypes: ['json', 'form'], multipart: true, formidable: { maxFileSize: 32 * 1024 * 1024, } })); // Routes... // connect defined routes as middleware to Koa app.use(router.routes()); // our app will listen on port 3000 app.listen(3000); console.log(' API listening on 3000');
但是,这不是一个生产设置(您应该使用日志记录、强制授权、处理错误等),但是这几行代码对于我将向您展示的示例来说可以正常工作。
注意:所有代码示例都使用 Koa,但数据验证代码也适用于 Express。 datalize 库还有一个实现 Express 表单验证的示例。
一个基本的 Node.js 表单验证示例
假设您在 API 中有一个 Koa 或 Express Web 服务器和一个端点,该端点在数据库中创建一个具有多个字段的用户。 有些字段是必需的,有些只能有特定的值或必须格式化为正确的类型。
你可以像这样编写简单的逻辑:
/** * @api {post} / Create a user * ... */ router.post('/', (ctx) => { const data = ctx.request.body; const errors = {}; if (!String(data.name).trim()) { errors.name = ['Name is required']; } if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) { errors.email = ['Email is not valid.']; } if (Object.keys(errors).length) { return ctx.error(400, {errors}); } const user = await User.create({ name: data.name, email: data.email, }); ctx.body = user.toJSON(); });
现在让我们重写这段代码并使用 datalize 验证这个请求:
const datalize = require('datalize'); const field = datalize.field; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email(), ]), (ctx) => { if (!ctx.form.isValid) { return ctx.error(400, {errors: ctx.form.errors}); } const user = await User.create(ctx.form); ctx.body = user.toJSON(); });
更短,更干净,很容易阅读。 使用 datalize,您可以指定字段列表并将尽可能多的规则(如果输入无效则引发错误的函数)或过滤器(格式化输入的函数)链接到它们。
规则和过滤器按照它们定义的顺序执行,所以如果你想先修剪一个字符串的空格,然后检查它是否有任何值,你必须在.trim()
.required()
。
然后,Datalize 将创建一个仅包含您指定的字段的对象(在更广泛的上下文对象中以.form
提供),因此您不必再次列出它们。 .form.isValid
属性告诉您验证是否成功。
自动错误处理
如果我们不想在每个请求中检查表单是否有效,我们可以添加一个全局中间件,如果数据没有通过验证,它会取消请求。
为此,我们只需将这段代码添加到我们创建 Koa/Express 应用程序实例的引导文件中。
const datalize = require('datalize'); // set datalize to throw an error if validation fails datalize.set('autoValidate', true); // only Koa // add to very beginning of Koa middleware chain app.use(async (ctx, next) => { try { await next(); } catch (err) { if (err instanceof datalize.Error) { ctx.status = 400; ctx.body = err.toJSON(); } else { ctx.status = 500; ctx.body = 'Internal server error'; } } }); // only Express // add to very end of Express middleware chain app.use(function(err, req, res, next) { if (err instanceof datalize.Error) { res.status(400).send(err.toJSON()); } else { res.send(500).send('Internal server error'); } });
而且我们不必再检查数据是否有效,因为 datalize 会为我们做这件事。 如果数据无效,它将返回带有无效字段列表的格式化错误消息。
查询验证
是的,您甚至可以非常轻松地验证您的查询参数——它不必仅用于POST
请求。 我们只是使用.query()
辅助方法,唯一的区别是数据存储在.data
对象而不是.form
中。
const datalize = require('datalize'); const field = datalize.field; /** * @api {get} / List users * ... */ router.post('/', datalize.query([ field('keywords').trim(), field('page').default(1).number(), field('perPage').required().select([10, 30, 50]), ]), (ctx) => { const limit = ctx.data.perPage; const where = { }; if (ctx.data.keywords) { where.name = {[Op.like]: ctx.data.keywords + '%'}; } const users = await User.findAll({ where, limit, offset: (ctx.data.page - 1) * limit, }); ctx.body = users; });
还有一个用于参数验证的辅助方法.params()
。 通过在路由器的.post()
方法中传递两个 datalize 中间件,可以一起验证查询和表单数据。
更多过滤器、数组和嵌套对象
到目前为止,我们在 Node.js 表单验证中使用了非常简单的数据。 现在让我们尝试一些更复杂的字段,如数组、嵌套对象等:
const datalize = require('datalize'); const field = datalize.field; const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record"; /** * @api {post} / Create a user * ... */ router.post('/', datalize([ field('name').trim().required(), field('email').required().email().custom((value) => { return new Promise((resolve, reject) => { dns.resolve(value.split('@')[1], 'MX', function(err, addresses) { if (err || !addresses || !addresses.length) { return reject(new Error(DOMAIN_ERROR)); } resolve(); }); }); }), field('type').required().select(['admin', 'user']), field('languages').array().container([ field('id').required().id(), field('level').required().select(['beginner', 'intermediate', 'advanced']) ]), field('groups').array().id(), ]), async (ctx) => { const {languages, groups} = ctx.form; delete ctx.form.languages; delete ctx.form.groups; const user = await User.create(ctx.form); await UserGroup.bulkCreate(groups.map(groupId => ({ groupId, userId: user.id, }))); await UserLanguage.bulkCreate(languages.map(item => ({ languageId: item.id, userId: user.id, level: item.level, )); });
如果我们需要验证的数据没有内置规则,我们可以使用.custom()
方法创建自定义数据验证规则(好名字,对吗?)并在那里编写必要的逻辑。 对于嵌套对象,有.container()
方法,您可以在其中指定字段列表,方法与datalize()
函数中相同。 您可以将容器嵌套在容器中,或者使用.array()
过滤器对其进行补充,将值转换为数组。 当.array()
过滤器在没有容器的情况下使用时,指定的规则或过滤器将应用于数组中的每个值。

所以.array().select(['read', 'write'])
会检查数组中的每个值是'read'
还是'write'
,如果不是,它将返回所有索引的列表错误。 很酷吧?
PUT
/ PATCH
在使用PUT
/ PATCH
(或POST
)更新数据时,您不必重写所有逻辑、规则和过滤器。 您只需添加一个额外的过滤器,如.optional()
或 .patch( .patch()
,如果请求中未定义任何字段,它将从上下文对象中删除任何字段。 ( .optional()
将使其始终是可选的,而.patch()
仅当 HTTP 请求的方法是PATCH
时才会使其成为可选。)您可以添加这个额外的过滤器,使其适用于在数据库中创建和更新数据。
const datalize = require('datalize'); const field = datalize.field; const userValidator = datalize([ field('name').patch().trim().required(), field('email').patch().required().email(), field('type').patch().required().select(['admin', 'user']), ]); const userEditMiddleware = async (ctx, next) => { const user = await User.findByPk(ctx.params.id); // cancel request here if user was not found if (!user) { throw new Error('User was not found.'); } // store user instance in the request so we can use it later ctx.user = user; return next(); }; /** * @api {post} / Create a user * ... */ router.post('/', userValidator, async (ctx) => { const user = await User.create(ctx.form); ctx.body = user.toJSON(); }); /** * @api {put} / Update a user * ... */ router.put('/:id', userEditMiddleware, userValidator, async (ctx) => { await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); }); /** * @api {patch} / Patch a user * ... */ router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => { if (!Object.keys(ctx.form).length) { return ctx.error(400, {message: 'Nothing to update.'}); } await ctx.user.update(ctx.form); ctx.body = ctx.user.toJSON(); });
使用两个简单的中间件,我们可以为所有POST
/ PUT
/ PATCH
方法编写大部分逻辑。 userEditMiddleware()
函数验证我们要编辑的记录是否存在,否则抛出错误。 然后userValidator()
对所有端点进行验证。 最后, .patch()
过滤器将从.form
对象中删除任何字段,如果它没有被定义并且请求的方法是PATCH
。
Node.js 表单验证附加功能
在自定义过滤器中,您可以获取其他字段的值并据此执行验证。 您还可以从上下文对象中获取任何数据,例如请求或用户信息,因为它们都在自定义函数回调参数中提供。
该库涵盖了一组基本的规则和过滤器,但您可以注册可用于任何字段的自定义全局过滤器,因此您不必一遍又一遍地编写相同的代码:
const datalize = require('datalize'); const Field = datalize.Field; Field.prototype.date = function(format = 'YYYY-MM-DD') { return this.add(function(value) { const date = value ? moment(value, format) : null; if (!date || !date.isValid()) { throw new Error('%s is not a valid date.'); } return date.format(format); }); }; Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') { return this.date(format); };
使用这两个自定义过滤器,您可以使用.date()
或.dateTime()
过滤器链接您的字段以验证日期输入。
文件也可以使用 datalize 进行验证:对于.file()
、 .mime()
和 .size .size()
) 等文件有特殊的过滤器,因此您不必单独处理文件。
立即开始编写更好的 API
我已经在几个生产项目中使用 datalize 进行 Node.js 表单验证,包括小型和大型 API。 它帮助我按时交付出色的项目,减轻压力,同时使它们更具可读性和可维护性。 在一个项目中,我什至使用它来验证 WebSocket 消息的数据,方法是编写一个简单的 Socket.IO 包装器,其用法与在 Koa 中定义路由几乎相同,所以这很好。 如果有足够的兴趣,我也可能会为此写一个教程。
我希望本教程能帮助您和我在 Node.js 中构建更好的 API,使用经过完美验证的数据而不会出现安全问题或内部服务器错误。 最重要的是,我希望它能为您节省大量时间,否则您将不得不投资编写额外的函数来使用 JavaScript 进行表单验证。