#
校验器

# 类校验

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

它的使用方式如下:

class RegisterValidator extends LinValidator {
  constructor() {
    super();
    this.username = [
      new Rule("isNotEmpty", "昵称不可为空"),
      new Rule("isLength", "昵称长度必须在2~10之间", 2, 10)
    ];
    this.group_id = new Rule("isInt", "分组id必须是整数,且大于0", {
      min: 1
    });
    this.email = [
      new Rule("isOptional"),
      new Rule("isEmail", "电子邮箱不符合规范,请输入正确的邮箱")
    ];
    this.password = [
      new Rule(
        "matches",
        "密码长度必须在6~22位之间,包含字符、数字和 _ ",
        /^[A-Za-z0-9_*&$#@]{6,22}$/
      )
    ];
    this.confirm_password = new Rule("isNotEmpty", "确认密码不可为空");
  }

  validateConfirmPassword(data) {
    if (!data.body.password || !data.body.confirm_password) {
      return [false, "两次输入的密码不一致,请重新输入"];
    }
    let ok = data.body.password === data.body.confirm_password;
    if (ok) {
      return ok;
    } else {
      return [false, "两次输入的密码不一致,请重新输入"];
    }
  }
}

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

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

TIP

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

所有可用的函数,你可以参考 validator.js 的文档

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

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

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

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

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

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

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

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 传入第三个参数,默认值。如:

new Rule("isOptional", "", "test@gmail.com"),

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

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

# 自定义规则函数

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

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

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

  validateConfirmPassword(data) {
    if (!data.body.password || !data.body.confirm_password) {
      return [false, "两次输入的密码不一致,请重新输入"];
    }
    let ok = data.body.password === data.body.confirm_password;
    if (ok) {
      return ok;
    } else {
      return [false, "两次输入的密码不一致,请重新输入"];
    }
  }

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

this.data = {
  body: ctx.request.body, // body -> body
  query: ctx.request.query, // query -> query
  path: ctx.params, // params -> path
  header: ctx.request.header // header -> header
};

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

property 来源 类型 作用
body ctx.request.body(上下文请求体) object ****
query ctx.request.query(上下文请求 query 参数) object ****
path ctx.param(路由参数) object ****
header ctx.request.header object ****

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

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

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

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

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

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

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

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

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

# 使用

# 校验

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

user.linPost(
  "userRegister",
  "/register",
  {
    permission: "注册",
    module: "用户",
    mount: false
  },
  async ctx => {
    // 使用
    const v = await new RegisterValidator().validate(ctx);
    // 取数据
    const username = v.get("body.username");
    await userDao.createUser(ctx, v);
    ctx.json(
      new Success({
        msg: "用户创建成功"
      })
    );
  }
);

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

TIP

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

# 取参

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

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

前端穿参时,由于 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 是否取解析后的参数,默认为 true boolean
defaultVal 默认值,如果参数为空时取默认值 any
const username = v.get("body.username");

v.get("body.username")会取出 body 下面的 username 参数。parsed 默认为 true 即 默认取转型后的参数,但此处的 username 为 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()这个函数取到。

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

validateStart (data) {
  const start = data.query.start;
  // 如果 start 为可选
  if (isOptional(start)) {
    return true;
  }
  const ok = checkDateFormat(start);
  if (ok) {
    this.parsed['query']['start'] = toDate(start);
    return ok;
  } else {
    return [false, "请输入正确格式开始时间", "start"];
  }
}

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

# 进阶

# 继承

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

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

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

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

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

# 别名

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

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

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

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

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