翻译:疯狂的技术宅
原文: https://www.toptal.com/nodejs...
本文首发微信公众号:jingchengyideng
欢迎关注,天天都给你推送新鲜的前端技术文章javascript
API 在执行过程当中的一个基本任务是数据验证。 在本文中,我想向你展现如何为你的数据添加防弹验证,同时返回风格良好的格式。前端
在 Node.js 中进行自定义数据验证既不容易也不快。 为了覆盖全部类型的数据,须要写许多函数。 虽然我已经尝试了一些 Node.js 的表单库 —— Express 和 Koa ——他们从未知足个人项目需求。 这些扩展库要么不兼容复杂的数据结构,要么在异步验证出现问题。java
这就是为何我最终决定编写本身的小巧而强大的表单验证库的缘由,它被称为 datalize。 它是可扩展的,所以你能够在任何项目中使用它,并根据你的要求进行自定义。 它可以验证请求的正文、查询或参数,还支持async
过滤器和复杂的JSON结构,如 数组 或 嵌套对象。node
Github:https://github.com/flowstudio...git
Datalize能够经过npm安装:github
npm install --save datalize
要解析请求的正文,你应该使用其余的库。 若是你尚未用过,我建议使用 koa-body for Koa 或 body-parser for Express。数据库
你能够将本教程用于已配置好的HTTP API服务器,也可使用如下简单的Koa HTTP服务器代码。express
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');
可是,这不是生产环境下的设置(你还应该使用 logging,强制 受权, 错误处理等),不过这几行代码用于向你正常展现后面的例子足够了。npm
注意:全部代码示例都基于 Koa,但数据验证代码也一样适用于 Express。 datalize 库还有一个实现 Express 表单验证的例子。json
假设你的 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 应用实例的 bootstrap 文件中。
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()
,若是未在请求中定义,它将从上下文对象中删除任何字段。 ( .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
的话。
在自定义过滤器中,你能够获取其余字段的值并根据该值执行验证。 还能够从上下文对象中获取任何数据,例如请求或用户信息,由于它们都是在自定义函数的回调参数中提供的。
该库涵盖了一组基本规则和过滤器,不过你能够注册能与任何字段一块儿使用的自定义全局过滤器,因此你没必要一遍又一遍地写相同的代码:
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()
等文件才有特殊的过滤器,因此你没必要单独处理文件。
对于小型和大型API,我已经在好几个生产项目中用 datalize 进行 Node.js 表单验证。 这有助于我按时提供优秀项目、减轻开发压力,同时使其更具可读性和可维护性。 在一个项目中,我甚至用它来经过对 Socket.IO 进行简单封装,来验证 WebSocket 消息的数据,其用法与在 Koa 中的定义路由几乎彻底相同,因此这很好用。 若是不少人有兴趣的话,我也能够为此编写一个教程。
我但愿本教程可以帮助你在 Node.js 中构建更好的API,并使用通过完美验证的数据,而不会出现安全问题或内部服务器错误。 最重要的是,我但愿它能为你节省大量时间,不然你将不得不用 JavaScript 投入大量时间来编写额外的函数进行表单验证。