推荐一个基于Node.js的表单验证库


Posted in Javascript onFebruary 15, 2019

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

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

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

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

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

配置

Datalize可以通过npm安装:

npm install --save datalize

要解析请求的正文,你应该使用其他的库。 如果你还没有用过,我建议使用 koa-body for Koa 或 body-parser for Express。

你可以将本教程用于已配置好的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');

但是,这不是生产环境下的设置(你还应该使用logging,强制 授权, 错误处理等),不过这几行代码用于向你正常展示后面的例子足够了。

注意:所有代码示例都基于 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 应用实例的 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 的话。

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() 等文件才有特殊的过滤器,所以你不必单独处理文件。

立即开始编写更好的API

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
实现变速回到顶部的JavaScript代码
May 09 Javascript
jQuery+.net实现浏览更多内容(改编php版本)
Mar 28 Javascript
jquery实现的一个文章自定义分段显示功能
May 23 Javascript
JavaScript设计模式之外观模式实例
Oct 10 Javascript
jQuery Easyui datagrid行内实现【添加】、【编辑】、【上移】、【下移】
Dec 19 Javascript
js+div+css下拉导航菜单完整代码分享
Dec 28 Javascript
利用jsonp与代理服务器方案解决跨域问题
Sep 14 Javascript
Vue中Quill富文本编辑器的使用教程
Sep 21 Javascript
vue轻量级框架无法获取到vue对象解决方法
May 12 Javascript
使用apifm-wxapi快速开发小程序过程详解
Aug 05 Javascript
JS Html转义和反转义(html编码和解码)的实现与使用方法总结
Mar 10 Javascript
JS+css3实现幻灯片轮播图
Aug 14 Javascript
微信小程序实现左右列表联动
May 19 #Javascript
webpack-url-loader 解决项目中图片打包路径问题
Feb 15 #Javascript
微信小程序实现单选选项卡切换效果
Jun 19 #Javascript
一秒学会微信小程序制作table表格
Feb 14 #Javascript
React通过redux-persist持久化数据存储的方法示例
Feb 14 #Javascript
微信小程序实现简易table表格
Jun 19 #Javascript
微信小程序制作表格的方法
Feb 14 #Javascript
You might like
PHP中的CMS的涵义
2007/03/11 PHP
Uncaught exception com_exception with message Failed to create COM object
2012/01/11 PHP
用php实现选择排序的解决方法
2013/05/04 PHP
关于php操作mysql执行数据库查询的一些常用操作汇总
2013/06/24 PHP
php递归遍历多维数组的方法
2015/04/18 PHP
CI框架整合widget(页面格局)的方法
2016/05/17 PHP
php常用正则函数实例小结
2016/12/29 PHP
如何使用json在前后台进行数据传输实例介绍
2013/04/11 Javascript
Javascript计算两个marker之间的距离(Google Map V3)
2013/04/26 Javascript
javascript中创建对象的几种方法总结
2013/11/01 Javascript
AngularJS ng-bind-template 指令详解
2016/07/30 Javascript
AngularJS的ng-click传参的方法
2017/06/19 Javascript
Vue.js实现网格列表布局转换方法
2017/08/25 Javascript
js判断文件类型大小并给出提示的实现方法
2018/01/03 Javascript
p5.js入门教程之平滑过渡(Easing)
2018/03/16 Javascript
关于js对textarea换行符的处理方法浅析
2018/08/03 Javascript
LayUi中接口传数据成功,表格不显示数据的解决方法
2018/08/19 Javascript
原生JS实现动态添加新元素、删除元素方法
2019/05/05 Javascript
ES6 proxy和reflect的使用方法与应用实例分析
2020/02/15 Javascript
[05:39]2014DOTA2国际邀请赛 DK晋级胜者组专访战队国士无双
2014/07/14 DOTA
使用PM2+nginx部署python项目的方法示例
2018/11/07 Python
详解用python自制微信机器人,定时发送天气预报
2019/03/25 Python
TensorFlow实现简单的CNN的方法
2019/07/18 Python
Cython编译python为so 代码加密示例
2019/12/23 Python
jupyter notebook 添加kernel permission denied的操作
2020/04/21 Python
使用OpenCV获取图片连通域数量,并用不同颜色标记函
2020/06/04 Python
不可轻视HTML5!App三年内将被html5顶替彻底消失
2015/11/18 HTML / CSS
几道PHP的面试题
2012/05/19 面试题
医院护士求职自荐信格式
2013/09/21 职场文书
4s客服专员岗位职责
2013/12/01 职场文书
实习自我评价怎么写
2013/12/02 职场文书
2014年学习部工作总结
2014/11/12 职场文书
医药公司采购员岗位职责
2015/04/03 职场文书
大学学生会主席竞选稿怎么写?
2019/08/19 职场文书
导游词之新疆-喀纳斯
2019/10/10 职场文书
CSS3 实现NES游戏机的示例代码
2021/04/21 HTML / CSS