大前端扫地僧之必备技能Zod
为什么需要Zod?
Zod作为一个现代的模式验证与类型推导工具,具有以下优势,使其成为前端进阶必备的工具:
- 与TypeScript无缝集成:Zod允许在定义模式的同时自动生成TypeScript类型,无需重复定义数据结构。
- 运行时验证:TypeScript只能在编译时检查类型,而Zod可以在运行时验证实际数据的正确性,避免潜在的错误。
- 轻量且直观:相比其他工具(如Yup和AJV),Zod的API更加简洁,无需学习复杂的JSON Schema。
- 功能强大:Zod支持嵌套结构、自定义验证、多种数据类型以及异步验证,几乎可以满足所有常见的验证需求。
Zod是什么?
Zod 是一个 TypeScript 优先的模式声明和验证库。它旨在以开发者友好的方式工作,目标是消除重复的类型声明。使用Zod,你只需声明一次验证器,Zod会自动推断出静态TypeScript类型,使得将简单类型组合成复杂数据结构变得容易。Zod具有以下特点:
- 零依赖:没有外部依赖,使得库更加轻量级。
- 适用于Node.js和现代浏览器:可以在多种环境中运行。
- 体积小:压缩后只有8kb,非常轻量。
- 不可变:方法(如
.optional()
)返回新实例,保证不可变性。 - 简洁、链式接口:提供简洁且易于链式调用的接口。
- 函数式方法:采用解析而非验证的方法。
- 也适用于纯JavaScript:即使不使用TypeScript,也可以使用Zod。
Zod如何使用呢?
1、基本数据
import { z } from 'zod';
const basicSchema = z.string();
console.log(basicSchema.parse('hello')); // hello
console.log(basicSchema.parse(123)); // ZodEror: Expected string, received number
原始数据类型如下:
// primitive values
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();
// empty types
z.undefined();
z.null();
z.void(); // accepts undefined
// catch-all types
// allows any value
z.any();
z.unknown();
// never type
// allows no values
z.never();
2、对象数据
const objectSchema = z.object({
name: z.string(),
age: z.number(),
});
console.log(objectSchema.parse({ name: 'hello', age: 18 })); // 验证通过 { name: 'hello', age: 18 }
3、数组数据
const arraySchema = z.array(z.string());
console.log(arraySchema.parse(['hello', 'world'])); // ['hello', 'world']
console.log(arraySchema.parse(['hello', 123])); // ZodError: Expected string, received number
4、枚举数据
const enumSchema = z.enum(['hello', 'world']);
console.log(enumSchema.parse('hello')); // hello
console.log(enumSchema.parse('hi')); // ZodError: Expected "hello" | "world", received "hi"
5、元组
const tupleSchema = z.tuple([z.string(), z.number()]);
console.log(tupleSchema.parse(['hello', 18])); // ['hello', 18]
console.log(tupleSchema.parse(['hello', 'world'])); // ZodError: Expected number, received string
6、字典
const dictionarySchema = z.record(z.string());
console.log(dictionarySchema.parse({ name: 'hello', age: '18' })); // { name: 'hello', age: '18' }
console.log(dictionarySchema.parse({ name: 123, age: '18' })); // ZodError: Expected string, received number
console.log(dictionarySchema.parse([1, 2, 3])); // ZodError: Expected object, received array
console.log(dictionarySchema.parse('hello')); // ZodError: Expected object, received string
7、map
const mapSchema = z.map(z.string(), z.number());
console.log(mapSchema.parse(new Map([['age', 18]]))); // Map {'age' => 18 }
console.log(mapSchema.parse(new Map([['age', '18']]))); // ZodError: Expected number, received string
8、set
const setSchema = z.set(z.string());
console.log(setSchema.parse(new Set(['hello', 'world']))); // Set {'hello', 'world' }
console.log(setSchema.parse(new Set([1, 2, 3]))); // ZodError: Expected string, received number
9、嵌套数据
const nestedSchema = z.object({
name: z.string(),
age: z.number(),
hobbies: z.array(z.string()),
});
console.log(nestedSchema.parse({ name: 'hello', age: 18, hobbies: ['reading', 'swimming'] })); // 验证通过 { name: 'hello', age: 18, hobbies: ['reading', 'swimming'] }
10、可选项数据
const optionalSchema = z.object({
name: z.string(),
age: z.number().optional(),
});
console.log(optionalSchema.parse({ name: 'hello' })); // { name: 'hello' }
console.log(optionalSchema.parse({ name: 'hello', age: 18 })); // { name: 'hello', age: 18 }
11、可空值
const nullableSchema = z.object({
name: z.string(),
age: z.number().nullable(),
});
console.log(nullableSchema.parse({ name: 'hello' })); // { name: 'hello' }
console.log(nullableSchema.parse({ name: 'hello', age: null })); // { name: 'hello', age: null }
12、默认值
const defaultSchema = z.object({
name: z.string(),
age: z.number().default(18),
});
console.log(defaultSchema.parse({ name: 'hello' })); // { name: 'hello', age: 18 }
Zod高级用法
1、自定义验证器
const customSchema = z.custom((value) => typeof value === 'string', 'Expected string, received number');
console.log(customSchema.parse('hello')); // hello
console.log(customSchema.parse(123)); // ZodError: Expected string, received number
2、递归实现
const baseCategorySchema = z.object({
name: z.string(),
});
const categorySchema = baseCategorySchema.extend({
chidlren: z.lazy(() => categorySchema.array()),
});
console.log(categorySchema.parse({
name: "大前端",
chidlren: [
{
name: "Node.js",
chidlren: [
{
name: "Vue.js",
chidlren: [],
},
],
},
],
})); // { name: '大前端', chidlren: [ { name: 'Node.js', chidlren: [Array] } ]}
3、对象
// .partial
const user = z.object({
email: z.string(),
username: z.string(),
});
// { email: string; username: string }
// 所有属性
const partialUser = user.partial();
// { email?: string | undefined; username?: string | undefined }
// 指定某个属性
const optionalEmail = user.partial({
email: true,
});
/*
{
email?: string | undefined;
username: string
}
*/
4、数组
数组至少包含一个元素,请使用 .nonempty()
const nonEmptyStrings = z.string().array().nonempty();
nonEmptyStrings.parse([]); // throws: "Array cannot be empty"
nonEmptyStrings.parse(["Ariana Grande"]); // passes
// .min/.max/.length
z.string().array().min(5); // must contain 5 or more items
z.string().array().max(5); // must contain 5 or fewer items
z.string().array().length(5); // must contain 5 items exactly
Zod 与 Nuxt3、@prisma/client ORM 使用
const RoleSchema = z.object({
roleName: z.string().min(1, { message: '角色名称不能为空' }),
roleKey: z.string().min(1, { message: '权限字符不能为空' }),
roleSort: z.number().min(0, { message: '排序不能小于0' }),
status: z.enum(['0', '1'], { message: '状态值只能为0或1' }),
});
// 检验input参数值
const body = await readBody(event);
RoleSchema.parse(body);
const UserSchema = z.object({
userName: z.string().min(1, { message: '用户名称不能为空' }),
email: z.string().email({ message: '邮箱格式不正确' }),
phonenumber: z.string().min(11, { message: '手机号码不能小于11位' }),
status: z.enum(['0', '1'], { message: '状态值只能为0或1' }),
sort: z.number().min(0, { message: '排序不能小于0' }),
});
const UserSchema2 = UserSchema.partial({
email: true,
phonenumber: true,
});
// 检验input参数值
const body = await readBody(event);
UserSchema2.parse(body);
// email、phonenumber 非必填,填写并验证邮箱和手机格式。
新增用户完整源码
import { Prisma } from '@prisma/client';
import prisma from '../../lib/prisma';
import { UserSchema2 } from '../../lib/zodSchema';
export default defineEventHandler(async (event) => {
try {
const body = await readBody(event);
UserSchema2.parse(body);
// 校验参数
const isUserName = await prisma.sys_user.findFirst({
where: {
userName: body.userName
}
})
if (isUserName) {
return {
code: 400,
msg: '用户名已存在',
}
}
// 参数
const rolesCreate: any = []
if (body.roles && Array.isArray(body.roles)) {
body.roles.map((roleId: Number) => {
rolesCreate.push({
roleId: roleId
})
})
}
const postsCreate: any = []
if (body.posts && Array.isArray(body.posts)) {
body.posts.map((postId: Number) => {
postsCreate.push({
postId: postId
})
})
}
const data: Prisma.sys_userCreateInput = {
deptId: body.deptId,
userName: body.userName,
nickName: body.nickName,
email: body.email,
phonenumber: body.phonenumber,
sex: body.sex,
status: body.status,
remark: body.remark,
posts: {
create: postsCreate
},
roles: {
create: rolesCreate
}
}
if (body.password) {
data.password = body.password;
}
const result = await prisma.sys_user.create({
data,
include: {
posts: true,
roles: true
}
});
return {
code: 200,
data: result,
msg: '创建成功'
}
} catch (error: any) {
const err = JSON.parse(error);
return {
code: 400,
msg: err[0].message || '未知错误'
}
} finally {
await prisma.$disconnect(); // 关闭连接
}
})
Zod的性能优化建议
关于Zod的性能优化,以下是一些实用的技巧和建议:
- 延迟验证(Lazy Validation): 对于大型对象,可以使用
z.lazy()
来延迟验证。这允许你创建一个验证器,该验证器在实际需要时才进行解析,从而提高性能。 - 部分验证(Partial Validation): 使用
z.pick()
或z.omit()
来只验证需要的字段。这样可以减少不必要的验证操作,提高效率。 - 缓存模式(Caching Schemas): 如果你频繁使用相同的模式,可以考虑缓存它们。这样可以避免重复创建模式实例,减少资源消耗。
- 异步验证(Asynchronous Validation): 对于复杂的验证逻辑,可以考虑使用异步验证器。这样可以避免阻塞主线程,提高应用的响应性。
- 可区分联合(Discriminated Unions): 使用
z.discriminatedUnion
方法来表示可区分联合。这可以使得Zod检查鉴别器键来确定使用哪个模式解析输入,从而提高解析效率,并让Zod报告更友好的错误。 - 优化错误处理: 合理配置错误处理,例如使用
.safeParse()
方法来安全地解析数据,并处理可能出现的错误,这样可以减少异常处理的开销。 - 合理配置和使用策略: 通过合理的配置和使用策略,可以在保证类型安全的同时,兼顾应用的性能需求。
这些性能优化技巧可以帮助你在使用Zod时提高应用的性能和响应速度。
Zod vs 其他验证库
Zod vs Yup
- 易用性:Yup和Zod都非常简单易学且易于使用,它们的语法非常相似,可以轻松地在两者之间切换代码库。
- 类型安全:Zod是为TypeScript设计的,提供了更强的类型安全保证。而Yup虽然支持TypeScript,但并不像Zod那样与TypeScript紧密集成。
- 性能:对于简单的验证规则和小型数据集,Zod和Yup的性能差异可能不大。但在处理复杂模式或大型数据集时,性能表现可能会有所不同。
- 生态系统和集成:Zod设计为轻量级,易于集成到项目中,无需担心额外的依赖。Yup则因其在表单验证中的流行而被广泛用于客户端验证,支持与流行表单库(如Formik)的集成。
Zod vs Joi
- 功能丰富性:Joi是一个成熟且功能丰富的验证库,常用于Node.js应用中,提供了广泛的验证规则和定制选项,适合复杂验证场景。
- 类型安全:Joi不是TypeScript优先的,这意味着在获得类型推断时可能需要额外的工作,而Zod则提供了与TypeScript类型的无缝集成。
- 性能:Joi因其丰富的功能而可能在小型项目中增加一些开销,而Zod则因其轻量级而受到青睐。
Zod vs AJV
- 性能和特性:AJV是一个高性能的JSON Schema验证器,支持最新的JSON Schema草案,并提供了广泛的验证选项,包括自定义格式、依赖关系和异步验证。
- 易用性和学习曲线:AJV虽然功能强大,但学习曲线较陡,可能需要更多的配置来处理高级用例。Zod则以其清晰直观的API和强大的类型推断能力,成为TypeScript项目的理想选择。
- 生态系统:AJV有着广泛的插件和集成支持,而Zod则更注重提供简单直接的验证解决方案。
综上所述,Zod以其TypeScript优先的设计、轻量级和直观的API,在现代JavaScript和TypeScript项目中越来越受欢迎。对于需要强大类型安全和简洁API的项目,Zod是一个优秀的选择。而Yup、Joi和AJV则在特定场景下,如复杂验证规则或特定生态系统中,可能更为合适。选择哪个库,最终取决于项目的具体需求、对TypeScript的熟悉程度以及数据验证的复杂性。