校验器

类校验

对于参数的校验,Lin 提供了类校验这种便捷,好用的方式,它会对ctx.request.body(上下文请求体)ctx.request.query(上下文请求query参数)ctx.request.header(上下文请求头)ctx.param(路由参数)这些参数进行统一校验,所以请保证你的参数名没有重复

它的使用方式如下:

  1. class RegisterValidator extends LinValidator {
  2. constructor() {
  3. super();
  4. this.nickname = [
  5. new Rule("isNotEmpty", "昵称不可为空"),
  6. new Rule("isLength", "昵称长度必须在2~10之间", 2, 10)
  7. ];
  8. this.group_id = new Rule("isInt", "分组id必须是整数,且大于0", {
  9. min: 1
  10. });
  11. this.email = [
  12. new Rule("isOptional"),
  13. new Rule("isEmail", "电子邮箱不符合规范,请输入正确的邮箱")
  14. ];
  15. this.password = [
  16. new Rule(
  17. "matches",
  18. "密码长度必须在6~22位之间,包含字符、数字和 _ ",
  19. /^[A-Za-z0-9_*&$#@]{6,22}$/
  20. )
  21. ];
  22. this.confirm_password = new Rule("isNotEmpty", "确认密码不可为空");
  23. }
  24. validateConfirmPassword(data) {
  25. if (!data.body.password || !data.body.confirm_password) {
  26. return [false, "两次输入的密码不一致,请重新输入"];
  27. }
  28. let ok = data.body.password === data.body.confirm_password;
  29. if (ok) {
  30. return ok;
  31. } else {
  32. return [false, "两次输入的密码不一致,请重新输入"];
  33. }
  34. }
  35. }

我们以RegisterValidator为例来详细的分析类校验器的使用。

  • 必须继承自LinValidator这个基类,且需要在构造函数中初始化校验规则。如我们在RegisterValidator 的构造函数中定义了 nickname,group_id 等校验规则。
  • 校验规则的规范。如 nickname 被初始化成一个数组,这个数组里面的必须是 Rule,当然也可以是 group_id 这样的单个 Rule。但是你必须给校验规则传入 Rule 或是Rule[]。LinValidator 会自动对前端传入数据中的 nickname 字段进行 Rule 规则的校验。
  • Rule 规范。Rule 的构造函数中,你可以传入三个参数。
    参数作用
    validateFunc校验函数
    message校验失败后返回的信息
    options校验函数的参数,如果validateFuncisOptional,则这个参数为默认值

TIP

注意,你传入的 validateFunc 为 string 类型时,你实际传入的是函数名,当然这些函数是有限的,这些函数实际上均是validator.js上校验函数。

所有可用的函数,你可以参考 validator.js的文档校验器 - 图1

我们把这些在constructor中显示申明的校验规则称为Rule校验。Rule 校验是校验器的基础校验方式,它足够方便,你只需要在构造函数中定义如下的校验字段:

  1. this.nickname = [
  2. new Rule("isNotEmpty", "昵称不可为空"),
  3. new Rule("isLength", "昵称长度必须在2~10之间", 2, 10)
  4. ];

校验器便可以自动帮助你在参数中找到nickname这个字段,并对这个字段的参数做 Rule校验。

nickname这个字段显示的被绑定了两个 Rule,它们以数组的形式组成,如果是一个Rule,直接绑定一个 Rule 即可,如:

  1. this.group_id = new Rule("isInt", "分组id必须是整数,且大于0", {
  2. min: 1
  3. });

nickname被校验的时候,参数会被链式的校验,即先进行isNotEmptyRule 校验,再进行isLengthRule 校验。如果nickname参数并未通过isNotEmpty这个 Rule,当前链便会中断。如果参数通过了这个链上所有 Rule,则参数的校验可判断为成功。

每个 Rule 的第三个参数是校验参数,它是validator.js中校验函数的校验参数。如isInt这个函数可以接受一些参数,我们在 Rule 的第三个参数中传入{ min: 1 },这些参数便可以被使用到 isInt 这个函数中。它会被这样调用:

  1. isInt("9", { min: 1 });

isOptional

下面我们需要着重了解isOptional这个 Rule 校验。单独拿它出来,因为它很特殊。它可以一定程度上左右我们的检验链。

首先,我们规定如果一个字段参数用到了isOptional,那么isOptionalRule 最好被放在校验链的首位。通过字面上,你肯定已经知道了isOptional的作用,它可以使一个字段的校验变的可选。

详细一点,如果一个字段被isOptional这个 Rule 所标记,那么这个字段参数可

:当这个参数字段存在的时候,校验器会对它做校验链上的其它校验。如email这个字段,它被标记为isOptional,如果这个参数存在(前端传入这个参数),那么 email会被后面的isEmailRule 所校验。

: 注意,此处的无并非没有。而是一定理念意义上的无。lin-validator 为了保持与wtforms 上的一致性。规定一下的参数情况可以被定义为

  • 参数不存在。即参数压根没有被前端传入。
  • null。如果一个参数为 null,那么它被定义为无。
  • 空字符串。如果一个参数为 "",它被认为无。
  • 空格字符串。如果一个参数为字符串,且只有空格,也被视作无。如: " "。若字段是的状态,那么它会被校验链上后面的 Rule 所校验,如果字段是的状态,那么这个字段会逃逸,它不会被校验链上后面的 Rule 所捕获。

当定义一个 Rule 为isOptional时,可以给该 Rule 传入第三个参数,默认值。如:

  1. new Rule("isOptional", "", "pedrogao1996@gmail.com"),

这段代码中的 10 就是默认值,请记住默认值是isOptional所独有的(目前来说)。

isOptionalRule 被赋有默认值时,这个字段就会发生变化。以email为例,当前端没有传入这个参数时,校验器中的email数据肯定是一个undefined。但是因为默认值的存在,这个email会被赋予默认值,即pedrogao1996@gmail.com

自定义规则函数

你可能已经发现了在RegisterValidator这个校验类中,有一个validateConfirmPassword的函数。

我们把以validate开头的类方法称之为规则函数,我们会在校验的时候自动的调用这些规则函数。

规则函数是校验器中另一种用于对参数校验的方式,它比显示的 Rule 校验具有更加的灵活性和可操作性。下面我们以一个小例子来深入理解规则函数:

  1. validateConfirmPassword(data) {
  2. if (!data.body.password || !data.body.confirm_password) {
  3. return [false, "两次输入的密码不一致,请重新输入"];
  4. }
  5. let ok = data.body.password === data.body.confirm_password;
  6. if (ok) {
  7. return ok;
  8. } else {
  9. return [false, "两次输入的密码不一致,请重新输入"];
  10. }
  11. }

首先任何一个规则函数,满足以validate开头的类方法,除validate()这个函数外。都会被带入一个重要的参数 data。data 是前端传入参数的容器,它的整体结构如下:

  1. this.data = {
  2. body: ctx.request.body, // body -> body
  3. query: ctx.request.query, // query -> query
  4. path: ctx.params, // params -> path
  5. header: ctx.request.header // header -> header
  6. };

请记住 data 参数是一个二级的嵌套对象,它的属性如下:

property来源类型作用
bodyctx.request.body(上下文请求体)object
queryctx.request.query(上下文请求 query 参数)object
pathctx.param(路由参数)object
headerctx.request.headerobject

data是所有参数的原始数据,前端传入的参数会原封不动的装进 data。通过这个 data我们可以很方便的对所有参数进行校验,如在validateConfirmPassword这个规则函数中,我们便对data.body中的passwordconfirm_password进行了联合校验。

我们通过对规则函数的返回值来判断,当前规则函数的校验是否通过。简单的理解,如果规则返回true,则校验通过,如果返回false,则校验失败。但是校验失败的情况下,我们需要返回一条错误信息,如:

  1. return [false, "两次输入的密码不一致,请重新输入"];

表示规则函数校验失败,并且错误信息为两次输入的密码不一致,请重新输入

返回值的所有可选项类似如下:

  1. validateNameAndAge() {
  2. // 表示校验成功
  3. return true;
  4. // 校验失败,并给定错误信息
  5. return [false,"message"]
  6. // 校验失败,并给定错误信息,以及错误信息的键为nameAndAge
  7. // 一般情况下,我们会默认生成键,如这个函数生成的键为 NameAndAge,当然你也可以选择自定义
  8. return [false,"message","nameAndAge"]
  9. }

规则函数除了通过返回值来判断失败之外,还可以通过抛出异常来提前结束规则函数并校验失败。如下:

  1. validateNameAndAge() {
  2. // 抛出异常,即校验失败
  3. throw new ParametersException({ msg: "Lin will carry you!" });
  4. // 返回true,表示校验成功
  5. return true;
  6. }

这两种方式都可以使规则函数校验失败,但是我们推荐你使用第一种方式,即 返回值方式

使用

校验

上面我们谈到了类校验的定义与检验函数的使用。那么校验器如何使用,我们从示例工程的项目看看上面定义的RegisterValidator如何去使用。

  1. user.linPost(
  2. "userRegister",
  3. "/register",
  4. {
  5. auth: "注册",
  6. module: "用户",
  7. mount: false
  8. },
  9. async ctx => {
  10. // 使用
  11. const v = await new RegisterValidator().validate(ctx);
  12. // 取数据
  13. const nickname = v.get("body.nickname");
  14. await userDao.createUser(ctx, v);
  15. ctx.json(
  16. new Success({
  17. msg: "用户创建成功"
  18. })
  19. );
  20. }
  21. );

通常,我们会在视图函数中初始化一个 validator,当视图函数被调用的时候,会初始化RegisterValidator,并调用validate方法,validate 方法会返回实例化的RegisterValidator 即v

TIP

此处的validate方法,我们必须使用 await 强制让它同步,只有参数校验成功后才能进入后面的逻辑,否则抛出异常退出当前视图函数。

取参

校验器的另一大功能便是取参。lin 提供的 validator 会对 int,float,boolean 和date 类型的参数做转型,当然这些转型是需要条件的,如:

  1. this.group_id = new Rule("isInt", "分组id必须是整数,且大于0", {
  2. min: 1
  3. });

前端穿参时,由于 js 弱语言的性质,group_id 以字符串的形式被传入,但是我们需要以int 的类型来使用它,因此我们对 group_id 加上一个 Rule。

校验器会被 group_id 做整型校验,即isInt。在做校验的同时,我们还是对 group_id做转型,即将 group_id 从 string 类型转化为 int 类型。被转化的数据会被存放在parsed里面。我们可以通过v.get()来取出相应的参数。

v.get()可接受三个参数,如下:

param说明类型
path参数路径string
parsed是否取解析后的参数,默认为 trueboolean
defaultVal默认值,如果参数为空时取默认值any
  1. const nickname = v.get("body.nickname");

v.get("body.nickname")会取出 body 下面的 nickname 参数。parsed 默认为 true 即默认取转型后的参数,但此处的 nickname 为 string,故转型后仍为 string。

v.get("body.group_id")会取出 int 型的 group_id,如需要原始的数据,你可以这样v.get("body.group_id", parsed = false)。有时候你需要取出整个 body 的参数,你可以这样 v.get("body")

刚才我们谈到isOptional这个 Rule 校验时,提到isOptional是可以携带一个默认值参数的,这个默认值你也可以通过v.get()这个函数取到。

接下来我们暂时回到自定义规则函数这一小节中,假如有如下规则函数(伪代码):

  1. validateStart (data) {
  2. const start = data.query.start;
  3. // 如果 start 为可选
  4. if (isOptional(start)) {
  5. return true;
  6. }
  7. const ok = checkDateFormat(start);
  8. if (ok) {
  9. this.parsed['query']['start'] = toDate(start);
  10. return ok;
  11. } else {
  12. return [false, "请输入正确格式开始时间", "start"];
  13. }
  14. }

start这个参数是一个字符串形式的时间,如2019-01-01 12:00:00,我们首先通过checkDateFormat这个函数对这个字符串进行校验。如果校验成功,我们还可以做另外一件事,将 start 这个参数从字符串类型转为 date 类型,并将转型后的数据赋值给this.parsed 中的对应参数路径,如 start 的原始路径是 data 的query.start,那么我们将解析后的参数赋值给this.parsedquery.start。这样你可以通过v.get()这个函数得到解析后的数据。

进阶

继承

校验器提供继承的方式,让你的参数可以被组合校验。

  1. class PositiveIdValidator extends LinValidator {
  2. constructor() {
  3. super();
  4. this.id = new Rule("isInt", "id必须为正整数", { min: 1 });
  5. }
  6. }

我们首先定义了一个PositiveIdValidator的校验器,它会被 id 这个参数进行正整数校验,一般情况下 id 的校验被使用的很普遍,其他的校验器也需要使用,但是我们又不想重新再写一遍。因此,我们可以继承PositiveIdValidator

  1. class UpdateUserInfoValidator extends PositiveIdValidator {
  2. constructor() {
  3. super();
  4. this.group_id = new Rule("isInt", "分组id必须是正整数", {
  5. min: 1
  6. });
  7. this.email = new Rule("isEmail", "电子邮箱不符合规范,请输入正确的邮箱");
  8. }
  9. }

这里UpdateUserInfoValidator继承了PositiveIdValidator,因此UpdateUserInfoValidator也可对 id 参数进行校验,而且扩展了 group_id 和 email两个参数的校验。

别名

lin-validator 不仅仅提供继承,还提供另一种解放劳动力的方式——别名。如:

  1. class PositiveIdValidator extends LinValidator {
  2. constructor() {
  3. super();
  4. this.id = new Rule("isInt", "id必须为正整数", { min: 1 });
  5. }
  6. }

PositiveIdValidator会对 id 参数进行校验,但是有时候参数的校验逻辑是一样的,但是参数的名字不相同。如 uid 这个参数,它跟 id 这个参数的 Rule 一样。那么我们是不是还需要重新再写一个校验器定义一个 uid 的 Rule 了。这可行,但不优雅。

  1. const v = await new PositiveIdValidator().validate(ctx, { id: "uid" });

我们可以通过上面的方式来给 id 一个别名,这个别名为 uid。当使用了别名之后,校验器不会对 id 这个参数做校验,但是会对 uid 这个参数做校验