前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >推荐一个基于 Node.js 的表单验证库 [每日前端夜话0x23]

推荐一个基于 Node.js 的表单验证库 [每日前端夜话0x23]

作者头像
疯狂的技术宅
发布2019-03-27 11:15:31
2.6K0
发布2019-03-27 11:15:31
举报
文章被收录于专栏:京程一灯京程一灯

每日前端夜话0x23

每日前端夜话,陪你聊前端。

每天晚上18:00准时推送。

正文共:6350 字

预计阅读时间: 15 分钟

翻译:疯狂的技术宅原文:https://www.toptal.com/nodejs/smart-node-js-form-validation

API 在执行过程中的一个基本任务是数据验证。 在本文中,我想向你展示如何为你的数据添加防弹验证,同时返回风格良好的格式。

在 Node.js 中进行自定义数据验证既不容易也不快。 为了覆盖所有类型的数据,需要写许多函数。 虽然我已经尝试了一些 Node.js 的表单库 —— Express 和 Koa ——他们从未满足我的项目需求。 这些扩展库要么不兼容复杂的数据结构,要么在异步验证出现问题。

使用 Datalize 在 Node.js 中进行表单验证

这就是为什么我最终决定编写自己的小巧而强大的表单验证库的原因,它被称为 datalize。 它是可扩展的,因此你可以在任何项目中使用它,并根据你的要求进行自定义。 它能够验证请求的正文、查询或参数,还支持async 过滤器和复杂的JSON结构,如 数组嵌套对象

Github:https://github.com/flowstudio/datalize

配置

Datalize可以通过npm安装:

代码语言:javascript
复制
1npm install --save datalize

要解析请求的正文,你应该使用其他的库。 如果你还没有用过,我建议使用 koa-body for Koa 【https://github.com/dlau/koa-body】或 body-parser for Express 【https://github.com/expressjs/body-parser】。

你可以将本教程用于已配置好的HTTP API服务器,也可以使用以下简单的Koa HTTP服务器代码。

代码语言:javascript
复制
 1const Koa = require('koa');
 2const bodyParser = require('koa-body');
 3
 4const app = new Koa();
 5const router = new (require('koa-router'))();
 6
 7// helper for returning errors in routes
 8app.context.error = function(code, obj) {
 9this.status = code;
10this.body = obj;
11};
12
13// add koa-body middleware to parse JSON and form-data body
14app.use(bodyParser({
15enableTypes: ['json', 'form'],
16multipart: true,
17formidable: {
18maxFileSize: 32 * 1024 * 1024,
19}
20}));
21
22// Routes...
23
24// connect defined routes as middleware to Koa
25app.use(router.routes());
26// our app will listen on port 3000
27app.listen(3000);
28
29console.log('? API listening on 3000');

但是,这不是生产环境下的设置(你还应该使用 logging【https://www.loggly.com/blog/node-js-libraries-make-sophisticated-logging-simpler/】,强制 授权【https://www.toptal.com/nodejs/secure-rest-api-in-nodejs】, 错误处理等【https://stackoverflow.com/questions/7310521/node-js-best-practice-exception-handling】),不过这几行代码用于向你正常展示后面的例子足够了。

注意:所有代码示例都基于 Koa,但数据验证代码也同样适用于 Express。 datalize 库还有一个实现 Express 表单验证的例子。

一个基本的Node.js表单验证案例

假设你的 API 中有一个 Koa 或 Express Web 写的服务和一个端点,用于在数据库中创建包含多个字段的用户数据。其中某些字段是必需的,有些字段只能具有特定值,或者必须格式化为正确的类型。

你可以像这样写一个简单的逻辑:

代码语言:javascript
复制
 1/**
 2 * @api {post} / Create a user
 3 * ...
 4 */
 5router.post('/', (ctx) => {
 6    const data = ctx.request.body;
 7    const errors = {};
 8
 9    if (!String(data.name).trim()) {
10        errors.name = ['Name is required'];
11    }
12
13    if (!(/^[\-0-9a-zA-Z\.\+_]+@[\-0-9a-zA-Z\.\+_]+\.[a-zA-Z]{2,}$/).test(String(data.email))) {
14        errors.email = ['Email is not valid.'];
15    }
16
17    if (Object.keys(errors).length) {
18        return ctx.error(400, {errors});
19    }
20
21    const user = await User.create({
22            name: data.name,
23            email: data.email,
24    });
25
26    ctx.body = user.toJSON();
27});

下面让我们重写这段代码并使用 datalize 验证这个请求:

代码语言:javascript
复制
 1const datalize = require('datalize');
 2const field = datalize.field;
 3
 4/**
 5 * @api {post} / Create a user
 6 * ...
 7 */
 8router.post('/', datalize([
 9    field('name').trim().required(),
10    field('email').required().email(),
11]), (ctx) => {
12    if (!ctx.form.isValid) {
13        return ctx.error(400, {errors: ctx.form.errors});
14    }
15
16    const user = await User.create(ctx.form);
17
18    ctx.body = user.toJSON();
19});

短小精悍并易于阅读。 使用 datalize,你可以指定字段列表,并为它们链接尽可能多的规则(用于判断输入是否有效并抛出错误的函数)或过滤器(用于格式化输入的函数)。

规则和过滤器的执行顺序与它们定义的顺序相同,所以如果你想要先切分含有空格的字符串,然后再检查它是否有值,则必须在 .trim() 之前定义 .required()

然后,Datalize 将只使用你指定的字段创建一个对象(在更广泛的上下文对象中以 .form 形式提供),因此你不必再次列出它们。 .form.isValid 属性会告诉你验证是否成功。

自动错误处理

如果我们不想检查表单是否对每个请求都有效,可以添加一个全局中间件,如果数据未通过验证,则取消请求。

为此,我们只需将这段代码添加到我们创建的 Koa / Express 应用实例的 bootstrap 文件中。

代码语言:javascript
复制
 1const datalize = require('datalize');
 2
 3// set datalize to throw an error if validation fails
 4datalize.set('autoValidate', true);
 5
 6// only Koa
 7// add to very beginning of Koa middleware chain
 8app.use(async (ctx, next) => {
 9    try {
10        await next();
11    } catch (err) {
12        if (err instanceof datalize.Error) {
13            ctx.status = 400;
14            ctx.body = err.toJSON();
15        } else {
16            ctx.status = 500;
17            ctx.body = 'Internal server error';
18        }
19    }
20});
21
22
23// only Express
24// add to very end of Express middleware chain
25app.use(function(err, req, res, next) {
26    if (err instanceof datalize.Error) {
27        res.status(400).send(err.toJSON());
28    } else {
29        res.send(500).send('Internal server error');
30    }
31});

而且我们不必检查数据是否有效,因为 datalize 将帮我们做到这些。 如果数据无效,它将返回带有无效字段列表的格式化错误消息。

查询验证

是的,你甚至可以非常轻松地验证查询参数——它不仅仅用于POST请求。 我们也可以只使用.query()辅助方法,唯一的区别是数据存储在 .data 对象而不是 .form 中。

代码语言:javascript
复制
 1const datalize = require('datalize');
 2const field = datalize.field;
 3
 4/**
 5 * @api {get} / List users
 6 * ...
 7 */
 8router.post('/', datalize.query([
 9    field('keywords').trim(),
10    field('page').default(1).number(),
11    field('perPage').required().select([10, 30, 50]),
12]), (ctx) => {
13    const limit = ctx.data.perPage;
14    const where = {
15    };
16
17    if (ctx.data.keywords) {
18        where.name = {[Op.like]: ctx.data.keywords + '%'};
19    }
20
21    const users = await User.findAll({
22        where,
23        limit,
24        offset: (ctx.data.page - 1) * limit,
25    });
26
27    ctx.body = users;
28});

还有一个辅助方法用于参数验证:.params()。 通过在路由的 .post() 方法中传递两个 datalize 中间件,可以同时对查询和表单数据进行验证。

更多过滤器,数组和嵌套对象

到目前为止,我们在 Node.js 表单验证中使用了非常简单的数据。 现在让我们尝试一些更复杂的字段,如数组,嵌套对象等:

代码语言:javascript
复制
 1const datalize = require('datalize');
 2const field = datalize.field;
 3const DOMAIN_ERROR = "Email's domain does not have a valid MX (mail) entry in its DNS record";
 4
 5/**
 6 * @api {post} / Create a user
 7 * ...
 8 */
 9router.post('/', datalize([
10    field('name').trim().required(),
11    field('email').required().email().custom((value) => {
12        return new Promise((resolve, reject) => {
13            dns.resolve(value.split('@')[1], 'MX', function(err, addresses) {
14                if (err || !addresses || !addresses.length) {
15                    return reject(new Error(DOMAIN_ERROR));
16                }
17
18                resolve();
19            });
20        });
21    }),
22    field('type').required().select(['admin', 'user']),
23    field('languages').array().container([
24        field('id').required().id(),
25        field('level').required().select(['beginner', 'intermediate', 'advanced'])
26    ]),
27    field('groups').array().id(),
28]), async (ctx) => {
29    const {languages, groups} = ctx.form;
30    delete ctx.form.languages;
31    delete ctx.form.groups;
32
33    const user = await User.create(ctx.form);
34
35    await UserGroup.bulkCreate(groups.map(groupId => ({
36        groupId,
37        userId: user.id,
38    })));
39
40    await UserLanguage.bulkCreate(languages.map(item => ({
41        languageId: item.id,
42        userId: user.id,
43        level: item.level,
44    ));
45});

如果我们需要验证的数据没有内置规则,我们可以用 .custom() 方法创建一个自定义数据验证规则(很不错的名字,对吗?)并在那里编写必要的逻辑。 对于嵌套对象,有 .container() 方法,你可以在其中用和 datalize() 函数相同的方式指定字段列表。 你可以将容器嵌套在容器中,或使用 .array() 过滤器对其进行补充,这些过滤器会将值转换为数组。 如果在没有容器的情况下使用 .array() 过滤器,则指定的规则或过滤器将被用于数组中的每个值。

所以 .array().select(['read', 'write']) 将检查数组中的每个值是 'read' 还是 'write' ,如果有任何一个值不是其中之一,则返回所有错误的索引列表。 很酷,对吧?

`PUT`/`PATCH`

在使用 PUT/PATCH (或 POST)更新数据时,你不必重写所有逻辑、规则和过滤器。 只需添加一个额外的过滤器,如 .optional().patch() ,如果未在请求中定义,它将从上下文对象中删除任何字段。 ( .optional() 将使它始终是可选的,而 .patch() 只有在 HTTP 请求的方法是 PATCH 时才会使它成为可选项。)你可以添这个额外的过滤器,以便它可以在数据库中创建和更新数据。

代码语言:javascript
复制
 1const datalize = require('datalize');
 2const field = datalize.field;
 3
 4const userValidator = datalize([
 5    field('name').patch().trim().required(),
 6    field('email').patch().required().email(),
 7    field('type').patch().required().select(['admin', 'user']),
 8]);
 9
10const userEditMiddleware = async (ctx, next) => {
11    const user = await User.findByPk(ctx.params.id);
12
13    // cancel request here if user was not found
14    if (!user) {
15        throw new Error('User was not found.');
16    }
17
18    // store user instance in the request so we can use it later
19    ctx.user = user;
20
21    return next();
22};
23
24/**
25 * @api {post} / Create a user
26 * ...
27 */
28router.post('/', userValidator, async (ctx) => {
29    const user = await User.create(ctx.form);
30
31    ctx.body = user.toJSON();
32});
33
34/**
35 * @api {put} / Update a user
36 * ...
37 */
38router.put('/:id', userEditMiddleware, userValidator, async (ctx) => {
39    await ctx.user.update(ctx.form);
40
41    ctx.body = ctx.user.toJSON();
42});
43
44/**
45 * @api {patch} / Patch a user
46 * ...
47 */
48router.patch('/:id', userEditMiddleware, userValidator, async (ctx) => {
49    if (!Object.keys(ctx.form).length) {
50        return ctx.error(400, {message: 'Nothing to update.'});
51    }
52
53    await ctx.user.update(ctx.form);
54
55    ctx.body = ctx.user.toJSON();
56});

With two simple middlewares, we can write most logic for all POST/PUT/PATCH methods. The userEditMiddleware() function verifies if the record that we want to edit exists and throws an error otherwise. Then userValidator() does the validation for all endpoints. Finally, the .patch() filter will remove any field from the .form object if it’s not defined and if the request’s method is PATCH.

使用两个简单的中间件,我们可以为所有 POST/PUT/PATCH 方法编写大多数逻辑。 userEditMiddleware() 函数验证我们要编辑的记录是否存在,否则便抛出错误。 然后 userValidator() 对所有端点进行验证。 最后 .patch() 过滤器将删除 .form 对象中的任何字段(如果其未定义)或者假如请求的方法是 PATCH 的话。

Node.js表单验证附加功能

在自定义过滤器中,你可以获取其他字段的值并根据该值执行验证。 还可以从上下文对象中获取任何数据,例如请求或用户信息,因为它们都是在自定义函数的回调参数中提供的。

该库涵盖了一组基本规则和过滤器,不过你可以注册能与任何字段一起使用的自定义全局过滤器,所以你不必一遍又一遍地写相同的代码:

代码语言:javascript
复制
 1const datalize = require('datalize');
 2const Field = datalize.Field;
 3
 4Field.prototype.date = function(format = 'YYYY-MM-DD') {
 5  return this.add(function(value) {
 6    const date = value ? moment(value, format) : null;
 7
 8    if (!date || !date.isValid()) {
 9      throw new Error('%s is not a valid date.');
10    }
11
12    return date.format(format);
13  });
14};
15
16Field.prototype.dateTime = function(format = 'YYYY-MM-DD HH:mm') {
17  return this.date(format);
18};

With these two custom filters you can chain your fields with .date() or .dateTime() filters to validate date input.

Files can also be validated using datalize: There are special filters just for files like .file(), .mime(), and .size() so you don’t have to handle files separately.

有了这两个自定义过滤器,你就可以用 .date().dateTime() 过滤器链接字段对日期输入进行验证。

文件也可以使用 datalize 进行验证:只有 .file(), .mime(), 和 .size() 等文件才有特殊的过滤器,所以你不必单独处理文件。

立即开始编写更好的API

对于小型和大型API,我已经在好几个生产项目中用 datalize 进行 Node.js 表单验证。 这有助于我按时提供优秀项目、减轻开发压力,同时使其更具可读性和可维护性。 在一个项目中,我甚至用它来通过对 Socket.IO 进行简单封装,来验证 WebSocket 消息的数据,其用法与在 Koa 中的定义路由几乎完全相同,所以这很好用。 如果很多人有兴趣的话,我也可以为此编写一个教程。

我希望本教程能够帮助你在 Node.js 中构建更好的API,并使用经过完美验证的数据,而不会出现安全问题或内部服务器错误。 最重要的是,我希望它能为你节省大量时间,否则你将不得不用 JavaScript 投入大量时间来编写额外的函数进行表单验证。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-02-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 京程一灯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 使用 Datalize 在 Node.js 中进行表单验证
    • 配置
      • 一个基本的Node.js表单验证案例
        • 自动错误处理
          • 查询验证
            • 更多过滤器,数组和嵌套对象
              • `PUT`/`PATCH`
                • Node.js表单验证附加功能
                  • 立即开始编写更好的API
                  相关产品与服务
                  消息队列 TDMQ
                  消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档