1.10 参数校验(下)

82 阅读3分钟

上面说过,参数校验可以在各个环节处理,只是上节我们选择的是在底层Model里进行过滤。

但多数情况下,我们都需要在最上层(控制层Controller)就进行校验。

比如我们有个属性是age,正常情况下是不是要求它必须是个数字?是不是应该大于0?

就以这个需求为例,我们看下怎么开发。

第1版

修改src/user/user.route.ts,先加一个接口:

export interface CreateUserDto {
  author: string;
  age: number;
}

再修改userRouter.post:

userRouter.post("/", async (context) => {
    const result = context.request.body({
      type: "json",
    });
    const value: CreateUserDto = await result.value;
    // 开始校验
    if (value.age === undefined || value.age === null) {
      context.response.status = 400;
      context.response.body = "age is required";
      return;
    }
    if (typeof value.age !== "number") {
      context.response.status = 400;
      context.response.body = "age must be a number";
      return;
    }
    if (value.age < 0) {
      context.response.status = 400;
      context.response.body = "age must be greater than or equal to 0";
      return;
    }
    if (!value.author) {
      context.response.status = 400;
      context.response.body = "author is required";
      return;
    }
    // 校验结束
    const id = await userService.addUser(value);
    context.response.body = id;
  })

http://localhost:8000/user的F12下执行:

fetch("/user", {
  method: "post",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ aa: "haha" }),
});

看是不是接口就报错了?

image.png

再分别传递非法的age、author自己验证下,这里就不占用篇幅了。

第2版

功能是实现了,但你有什么感想?有没有觉得麻烦?这么简单的一个校验,就用了20行代码。这还是只有2个属性,4个规则,要是再加上性别、头像、昵称这些,不知道你会不会崩溃?

这种代码就属于能工作的面条代码,在真正生产实践中是要极力避免的。

记住这条准则,类似的代码出现2处,可能是程序设计问题,如果出现了3处,那肯定是程序设计问题了。

事实上,这与前端表单校验非常相似,你只需要设计好对应的规则和校验失败的错误信息,再用某种方式开始校验就可以了。

先加一个规则的接口:

interface RuleItem {
  validate: (value: any) => boolean;
  message: string;
}

type UserKey = keyof CreateUserDto;

type Rule = { [key in UserKey]: RuleItem[] };

再修改刚才的user.route.ts:

userRouter.post("/", async (context) => {
    const result = context.request.body({
      type: "json",
    });
    const value: CreateUserDto = await result.value;
    const rules: Rule = {
      age: [
        {
          validate: (value: unknown) => (value !== undefined || value !== null),
          message: "age is required",
        },
        {
          validate: (value: unknown) => typeof value === "number",
          message: "age must be number",
        },
        {
          validate: (value: number) => typeof value === "number" && value >= 0,
          message: "age must be greater than or equal to 0",
        },
      ],
      author: [{
        validate: (value: unknown) => !!value,
        message: "author is required",
      }],
    };

    const errors: string[] = [];
    Object.keys(rules).forEach((key) => {
      const rule: RuleItem[] = rules[key as UserKey];
      rule.forEach((item: RuleItem) => {
        if (!item.validate(value[key as UserKey])) {
          errors.push(item.message);
        }
      });
    });

    if (errors.length > 0) {
      context.response.status = 400;
      context.response.body = errors.join(",");
      return;
    }

    const id = await userService.addUser(value);
    context.response.body = id;
  })

现在逻辑是不是要优雅一些?

可能有人会说,这代码怎么看着更多了?事实上,这种校验规则都是可以枚举出来的,常用的也就几十种,早有人封装了类似的库。我们再看下一版。

第3版

deps.ts

修改deps.ts,新增一条:

export {
  IsNumber,
  IsOptional,
  IsString,
  Min,
  validate,
  validateOrReject,
  ValidationError,
} from "https://deno.land/x/deno_class_validator@v1.0.0/mod.ts";

utils.ts

新建src/utils.ts:

// deno-lint-ignore-file no-explicit-any
import { validateOrReject, ValidationError } from "../deps.ts";
import type { Constructor } from "./schema.ts";

export async function validate(
  Cls: Constructor,
  value: Record<string, any>,
): Promise<string[]> {
  const post = new Cls();
  Object.assign(post, value);
  const msgs: string[] = [];
  try {
    await validateOrReject(post);
  } catch (errors) {
    // console.debug(errors);
    errors.forEach((err: ValidationError) => {
      if (err.constraints) {
        Object.values(err.constraints).forEach((element) => {
          msgs.push(element);
        });
      }
    });
  }
  return msgs;
}

user.dto.ts

新建一个src/user/user.dto.ts:

import { IsNumber, IsString, Min } from "../../deps.ts";

export class CreateUserDto {
  @IsString() // 参数可配置报错信息
  author: string;

  @IsNumber()
  @Min(0)
  age: number;
}

可配置的规则很多,详情见deno.land/x/deno_clas…,还有个比较常用的是IsOptional,代表这个参数可传可不传。

user.route.ts

删除上一版代码。修改post:

import { validate } from "../utils.ts";
import { CreateUserDto } from "./user.dto.ts";

userRouter.post("/", async (context) => {
    const result = context.request.body({
      type: "json",
    });
    const value: CreateUserDto = await result.value;
    const errors = await validate(CreateUserDto, value);
    if (errors.length > 0) {
      context.response.status = 400;
      context.response.body = errors.join(",");
      return;
    }

    const id = await userService.addUser(value);
    context.response.body = id;
  })

这样代码就比较精减了。

作业

思考下,上面代码还能再瘦身吗?