上面说过,参数校验可以在各个环节处理,只是上节我们选择的是在底层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" }),
});
看是不是接口就报错了?
再分别传递非法的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;
})
这样代码就比较精减了。
作业
思考下,上面代码还能再瘦身吗?