个性化阅读
专注于IT技术分析

智能Node.js表单验证

本文概述

在API中执行的基本任务之一是数据验证。在本文中, 我想向你展示如何以一种可以返回格式正确的方式为数据添加防弹验证。

在Node.js中进行自定义数据验证既不容易也不快捷。你需要编写许多功能才能涵盖任何类型的数据。虽然我尝试了Express和Koa的一些Node.js表单数据库, 但它们从未满足我的项目需求。扩展库存在问题, 并且库无法使用复杂的数据结构或异步验证。

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

因此, 我最终决定编写自己的微型但功能强大的表单验证库, 称为datalize。它是可扩展的, 因此你可以在任何项目中使用它, 并根据需要对其进行自定义。它可以验证请求的正文, 查询或参数。它还支持异步过滤器和复杂的JSON结构, 例如数组或嵌套对象。

设定

可以通过npm安装Datalize:

npm install --save datalize

要解析请求的正文, 你应该使用单独的库。如果你还没有使用过, 我建议将Koa-body用作Koa或将body-parser用作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');

但是, 这不是生产设置(你应该使用日志记录, 强制执行授权, 处理错误等), 但是以下几行代码对于我将向你展示的示例很好用。

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

基本的Node.js表单验证示例

假设你有一个Koa或Express网络服务器, 并且API中有一个终结点, 该终结点创建的用户在数据库中具有多个字段。有些字段是必填字段, 有些只能具有特定值, 或者必须将其格式化为正确的类型。

你可以这样编写简单的逻辑:

/**
 * @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\.\+_][email protected][\-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, 你可以指定字段列表, 并根据需要链接到任意多个规则(如果输入无效则抛出错误的函数)或过滤器(格式化输入的函数)。

规则和过滤器的执行顺序与定义的顺序相同, 因此, 如果要先为空白修剪字符串, 然后检查是否有任何值, 则必须在.required()之前定义.trim()。

然后, 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()方法中传递两个数据化中间件, 可以一起验证查询和表单数据。

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

到目前为止, 我们已经在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

我已经在多个生产项目中使用Datalize进行Node.js表单验证, 无论大小API。它帮助我按时交付了出色的项目, 并减轻了压力, 同时使它们更具可读性和可维护性。在一个项目中, 我什至用它通过围绕Socket.IO编写一个简单的包装来验证WebSocket消息的数据, 其用法与在Koa中定义路由几乎相同, 所以很好。如果有足够的兴趣, 我也可以为此编写一个教程。

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

赞(0)
未经允许不得转载:srcmini » 智能Node.js表单验证

评论 抢沙发

评论前必须登录!