egg-acr 专为 egg 开发的异步验证组件
发布于 6 年前 作者 seekcx 2900 次浏览 来自 分享

开发原因

在实际项目中经常需要对表单数据做异步验证,比如验证用户名是否唯一等等。最开始一直使用 egg-validate,但不支持异步验证,遇到类似的需求就只能增加判断,但是因为前端偷懒的缘故(因为要封装成和验证器一样的错误类型,便于进行展示),然后又实现了一个模仿 表单 错误的方法。

但是这种情况对强迫症是无法接受的,于是开始寻找其它的验证组件。

找到了两个,joi 和 yup,但是 joi 的问题也是不支持异步验证,而 yup 虽然支持异步验证(还为此封装了 egg-yup),但是使用了一段时间后发现验证顺序有十分严重的 bug。

其实挺喜欢 joi 和 yup 的验证风格,简单明了,不容易出错,还能够很方便的自定义错误提示。于是便参照着这两颗大树开发了 acr

️功能亮点

  1. 支持异步验证
  2. 提供了多种方式进行国际化
  3. 和 yup 和 joi 一样,链式操作
  4. 扩展性非常强
  5. 支持 Typescript
  6. 可以对参数进行命名
  7. 逻辑验证

快速上手

在 egg 中使用:

const { phone, password } = await ctx.validate({
    phone: acr.string('手机号').required().phone(),
    password: acr.string('密码').required().password(),
});

每个被验证的数据,后面都声明需要进行的验证,称做验证链。

验证链上,第一个方法是类型声明,后面跟着若干个验证方法。不同的类型有不同的验证方法,即便是同名的方法,在不同的类型上验证行为也不尽一致。比如:

acr.string().min(6); // 指文字长度最小是 6

acr.number().min(6); // 指数字大小最小是6

验证类型方法接收第一个参数可以为当前的被验证的参数命名,因为我们经常遇到哪怕是同样的手机号验证在页面上的展示的名字也不一样。因为错误消息可以进行简单的模板替换,对参数命名可以让提示更加友好。比如

await ctx.validate({
    password: acr.string('密码').required(),
	username: acr.string('用户名').required(),
});

如果用户 username 和 password 都没有填写的话,会提示 密码必填用户名必填,而不是只简单的提示必填。

这还不够,为了实现更加复杂的验证需求,最近又新增了一个逻辑验证功能。

比如我们需要在类型为个人的时候判断用户的年龄是否大于 18 岁,否则不做任何处理

await ctx.validate({
    type: acr.string().required().in([ 'personal', 'business', 'custom' ]),
    age: acr.when('type', 'personal', () => {
        return acr.string().required().min(18);
    }),
});

可能还需要在类型为 business 判断年龄是否大于 36 岁(举个栗子,应该没有这么无聊的产品)

await ctx.validate({
    type: acr.string().required().in([ 'personal', 'business', 'custom' ]),
    age: acr.when('type', 'personal', () => {
        return acr.number().required().min(18);
    }).when('type', 'business', () => {
        return acr.number().required().min(36);
    }),
});

其它任何类型的时候年龄都必须大于 50 岁。

await ctx.validate({
    type: acr.string().required().in([ 'personal', 'business', 'custom' ]),
    age: acr.when('type', 'personal', () => {
        return acr.number().required().min(18);
    }).when('type', 'business', () => {
        return acr.number().required().min(36);
    }).other(() => {
        return acr.number().required().min(50);
    }),
});

扩展

扩展 acr 十分方便,也是可以链式定义的。

acr.type('string')
    .define('nickname', (value: string, { params }) => {
        const is = /^(?!_)(?!.*?_$)[a-zA-Z0-9_\u4e00-\u9fa5]{2,12}$/.test(value);

        return is ? true : params[0] || false;
    })
    .define('password', (value: string, { params }) => {
        const is = /^[\w\`\~\!\@\#\$\%\^\&\*\(\)\-\_\=\+\[\]\{\}\|\;\:\'\"\,\<\.\>\/\?]{4,24}$/.test(value);

        return is ? true : params[0] || false;
    });

定义一个异步验证方法:

// 一个简单的唯一判断规则,基于 egg-mongoose
acr.type('string')
    .define('unique', async (value: string, { params, context: { app } }) => {
        const [ collection, path ] = params;
        const record = await (app as Application).model[upperFirst(collection)].findOne({
            [path]: value,
        }, { _id: 1 });

        return isNil(record) ? true : get(params, '2') || false;
    });

使用上面的定义:

await ctx.validate({
    nickname: acr.string('昵称').required().nickname().unique('user', 'nickname'),
    password: acr.string('密码').required().password(),
});

相关链接

acr: https://github.com/seekcx/acr egg-acr: https://github.com/seekcx/egg-acr acr 文档地址:https://seek.gitbook.io/acr

欢迎大家的 star 和 pr,和各种 issue 帮助完善这个组件。🙏🙏

4 回复

这个用的 Proxy吗

来自酷炫的 CNodeMD

@atian25 谢谢大佬

@zswnew 不,用的 defineProperty

回到顶部