当从不同服务器获取数据时,我们收到的数据可能并不总是符合预期的格式。这可能导致我们应用程序中的错误。因此,在处理外部数据时,要格外谨慎至关重要。
TypeScript 单独并不足以进行模式验证,因此我们需要引入专门的模式验证器,比如 Zod 或 Yup。
在全栈应用程序中,前端和后端都与多个外部数据源进行交互,包括:
前端:
- 后端的 API 请求
- 第三方 API 数据
- 用户输入表单
- 本地存储
- URL 数据
后端:
- 前端的 API 请求
- 第三方 API 数据
- 环境变量
- Webhooks
- 文件系统
- 数据库
- URL
通过实施健壮的模式验证,我们可以确保数据完整性并防止潜在错误,从而使应用程序更可靠和安全。
全栈应用程序中获取数据以及需要模式验证器的清晰理解图片。
为什么 TypeScript 还不够?
我正在使用React/Next.js更好地理解为什么需要在TypeScript旁边使用Zod。假设我们有一个product/page.tsx
页面,我们想要从/api/product
获取数据。
product/page.tsx
"使用客户端";
import React, { use, useEffect } from "react";
type Product = { name: string; price: number; };
const Product = () => {
useEffect(() => {
fetch("/api/product")
.then((res) => res.json())
.then((data: Product) => {
console.log(data.name.toUpperCase());
});
}, []);
return <div>产品</div>;
};
export default 产品;
/api/product
import { NextResponse } from "next/server";
export async function GET(request: Request) { const product = { name: "Product 1", price: 100, };
return NextResponse.json(product);
假设有独立的后端和前端团队。他们更改后端的返回类型如此,那么我们的应用程序将崩溃。在运行时,您无法确切知道数据的形状会是什么,因此可能会导致应用程序崩溃。
api/product/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) { const product = { id: 1, price: 100, };
return NextResponse.json(product);
在这种情况下,我们的应用可能会崩溃。这就是为什么我们需要在TypeScript旁边使用适当的验证模式。
通过可选链可以防止应用程序崩溃。
product/page.tsx
"使用客户端";
import React, { use, useEffect } from "react";
type Product = {
name: string;
price: number;
};
const Product = () => {
useEffect(() => {
fetch("/api/product")
.then((res) => res.json())
.then((product: Product) => {
console.log(product?.name?.toUpperCase());
});
}, []);
返回 <div>产品</div>; };
export default 产品;
但如果产品实际上不存在,我们想向用户显示友好的错误消息。这对我们没有帮助,如果我们的返回类型存在,但是不同类型,比如价格是字符串,那么可选链类型根本没有帮助。
api/product/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) { const product = { id: 1,
价格:"$100",
};
返回 NextResponse.json(product);
product/page.tsx
"使用客户端";
import React, { use, useEffect } from "react";
type Product = {
name: string;
price: number;
};
const Product = () => {
useEffect(() => {
fetch('/api/product')
.then((res) => res.json())
.then((product: Product) => {
console.log(product?.name?.toUpperCase());
});
}, []);
返回 <div>产品</div>;
};
export default Product;
然后它会导致我们的应用程序崩溃:
你可以检查数字的类型。像
product/page.tsx
"使用客户端";
import React, { use, useEffect } from "react";
type Product = {
name: string;
price: number;
};
const Product = () => {
useEffect(() => {
fetch("/api/product")
.then((res) => res.json())
.then((product: Product) => {
console.log(product?.name?.toUpperCase());
if (typeof product?.price === "number") {
console.log(product?.price?.toFixed(2));
}
});
}, []);
返回 <div>产品</div>; };
export default 产品;
你可以看到这是非常混乱的代码,这不是一个健壮的解决方案。这就是模式验证器发挥作用的地方,其中一个流行的是Zod。还有其他的,比如Yup,它们都有类似的功能,即简单验证数据。
示例 1–2: 用于 API 请求和第三方 API 的 Zod:
yarn add zod or npm i zod
Zod 是运行时依赖项,我们可以在运行时使用。
product/page.tsx
"使用客户端";
import React, { use, useEffect } from "react";
import { z } from "zod";
type Product = {
name: string;
price: number;
};
const productSchema = z.object({ name: z.string(), price: z.number(), });
const Product = () => {
useEffect(() => {
fetch("/api/product")
.then((res) => res.json())
.then((product: Product) => {
const validatedProduct = productSchema.safeParse(product);
if (!validatedProduct.success) {
console.error(validatedProduct.error.message);
return;
}
console.log(validatedProduct.data);
});
}, []);
返回 <div>产品</div>;
};
export default 产品;
api/product/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) { const product = { id: 1, price: "$100", };
返回 NextResponse.json(product);
价格不正确,但我们的应用程序没有崩溃。Zod 在控制台中显示了漂亮的错误消息:
这里显示问题所在的关键是路径键和值是名称。
如果我们更正返回类型,我们将在控制台中看不到错误。
api/product/route.ts
import { NextResponse } from "next/server";
export async function GET(request: Request) { const product = { name: "Product 1", price: 100, };
返回 NextResponse.json(product);
我们可以将其用作单一真相来源。比如,如果我们需要一个帮助函数,其中需要产品类型,那么我们也可以使用 Zod。
product/page.tsx
"使用客户端";
import React, { use, useEffect } from "react";
import { z } from "zod";
const productSchema = z.object({ name: z.string(), price: z.number(), });
type Product = z.infer<typeof productSchema>;
const getProductPrice = (product: Product) => { return product.price; };
const Product = () => {
useEffect(() => {
fetch("/api/product")
.then((res) => res.json())
.then((product: unknown) => {
const validatedProduct = productSchema.safeParse(product);
if (!validatedProduct.success) {
console.error(validatedProduct.error.message);
return;
}
console.log(validatedProduct.data);
});
}, []);
返回 <div>产品</div>; };
export default 产品;
如果我们更改模式,产品类型将自动更新。
如果我们在自己的后端进行 API 请求,将会有一个更健壮的解决方案,称为 tRPC,这需要更多的设置。因此,如果我控制我们自己的 API 路由,我们可能会想要使用 tRPC。在底层执行类似操作。如果我们无法控制后端,我们可以在应用程序运行之前使用 React Query 和 Zod。
示例 3:来自外部来源的表单数据
我们有三个输入字段 email、password 和 confirmPassword,我们可以通过 react-hook-form / formic + zod 运行它。react-hook-form 有助于:
⇒ 表单验证
⇒ 错误和加载状态
⇒ 提高性能并防止不必要的重新渲染
在这里,zod 帮助我们验证模式。要在我们的应用程序中添加 react-hook-form:
yarn add react-hook-form
yarn add @hookform/resolvers
从 Zod 的角度来看,我们需要它的原因是这些表单数据将在我们的后端中使用。因此,我们希望创建一个单一的真相文件,以帮助我们验证数据。当后端接收到数据时,它也可能会验证数据,因为我们不能信任来自客户端的所有内容。因此,如果我们在客户端和后端都验证模式,那将是非常好的。但是 react-hook-form 仅在客户端进行验证,我们无法在后端重用它。然而,如果我们使用 Zod 进行验证,我们可以在客户端和后端都使用该模式。
创建一个名为 lib/types.ts
的文件夹和文件,此架构将同时用于客户端和后端。
import { z } from "zod";
export const signUpSchema = z
.object({
email: z.string().email(),
password: z
.string()
.min(10, "密码必须至少为 10 个字符")
.max(100),
confirmPassword: z.string().min(10).max(100),
})
.refine((data) => data.password === data.confirmPassword, {
message: "密码不匹配",
path: ["confirmPassword"],
});
export type TsignUpSchema = z.infer<typeof signUpSchema>;
创建一个名为: signup/page.tsx
的页面
import FormWithReactHookFormAndZod from "@/components/form-with-rhf-and-zod"; import React from "react";
const SignUp = () => {
return (
<div className="text-black">
<FormWithReactHookFormAndZod />
</div>
);
};
导出默认的注册;
创建一个组件文件夹,将表单布局复制到其中:
components/form-with-rhf-and-zod.tsx
"使用客户端";
import { TsignUpSchema, signUpSchema } from "@/lib/types"; import React from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod";
export default function FormWithReactHookFormAndZod() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
reset,
setError,
} = useForm<TsignUpSchema>({
解析器: zodResolver(signUpSchema),
};
const onSubmit = async (data: TsignUpSchema) => {
const response = await fetch("/api/signup", {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
},
});
const responseData = await response.json();
if (!response.ok) {