大前端扫地僧之必备技能Zod

作者: tww844475003 分类: 前端开发 发布时间: 2024-12-07 23:07

为什么需要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的性能优化,以下是一些实用的技巧和建议:

  1. 延迟验证(Lazy Validation): 对于大型对象,可以使用z.lazy()来延迟验证。这允许你创建一个验证器,该验证器在实际需要时才进行解析,从而提高性能。
  2. 部分验证(Partial Validation): 使用z.pick()z.omit()来只验证需要的字段。这样可以减少不必要的验证操作,提高效率。
  3. 缓存模式(Caching Schemas): 如果你频繁使用相同的模式,可以考虑缓存它们。这样可以避免重复创建模式实例,减少资源消耗。
  4. 异步验证(Asynchronous Validation): 对于复杂的验证逻辑,可以考虑使用异步验证器。这样可以避免阻塞主线程,提高应用的响应性。
  5. 可区分联合(Discriminated Unions): 使用z.discriminatedUnion方法来表示可区分联合。这可以使得Zod检查鉴别器键来确定使用哪个模式解析输入,从而提高解析效率,并让Zod报告更友好的错误。
  6. 优化错误处理: 合理配置错误处理,例如使用.safeParse()方法来安全地解析数据,并处理可能出现的错误,这样可以减少异常处理的开销。
  7. 合理配置和使用策略: 通过合理的配置和使用策略,可以在保证类型安全的同时,兼顾应用的性能需求。

这些性能优化技巧可以帮助你在使用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的熟悉程度以及数据验证的复杂性。

扩展阅读

zod中文网

前端开发那点事
微信公众号搜索“前端开发那点事”

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注