使用 Zod 在 Remix 中进行表单验证
Remix是一个很棒的 React 框架,用于构建现代 SSR(服务器端渲染)Web 体验。这意味着我们可以在单个 Remix 应用程序中同时使用后端和前端。 Remix 非常独特,充满了强大的功能。最不同的之一是在表单中工作时。 Remix 带回了处理表单的传统方法。
Remix 提供了我们可以用来执行服务器端操作和访问表单数据的函数(称为操作和加载器)。有了这些功能,我们不再需要向前端提供 JavaScript 来提交表单,从而减少了浏览器的 javascript 块。
当我们进行验证时,我个人选择的库之一是Zod。 Zod 是一个 TypeScript 优先的模式声明和验证库。使用 Zod,我们声明一次验证器,Zod 将自动推断静态 TypeScript 类型。将更简单的类型组合成复杂的数据结构很容易。
为什么需要验证?
我们希望用户提交的数据安全且符合预期。我们在构建应用程序时需要验证登录的三个主要原因。
-
我们希望以正确的格式获取正确的数据。如果我们的用户数据以错误的格式存储、不正确或完全被遗漏,我们的应用程序将无法正常工作。
-
我们要保护用户的数据。强制我们的用户输入安全密码可以更轻松地保护他们的帐户信息。
-
我们要保护自己。恶意用户可以通过多种方式滥用未受保护的表单来破坏应用程序。
我们在建造什么
我们正在使用 Zod 在 Remix 中从头开始构建表单验证。很多时候我们需要在服务器端验证我们的数据。这是我们可以拥有的杀手级组合,这样我们从 API 接收到的数据将是完全类型化的,并且我们只获得我们需要的有效数据。我们将强制用户在保存数据之前提交我们打算接收的数据以验证路由中的用户输入,无论我们想要将数据存储在哪里。
混音形式
Remix 提供了一个自定义表单组件,我们可以与原生 HTML 元素 一样工作。使用 React 时,我们需要在所有表单字段中监听 onChange 事件并更新我们的状态。但是,Remix 使用来自网络的 formData() API 的表单数据。
Form 是一个 Remix-aware 和增强的 HTML 表单组件,它的行为类似于普通表单,只是与服务器的交互是使用 fetch 而不是新文档请求。表单会自动向当前页面路由发出 POST 请求。但是,我们可以将其配置为 PUT 和 DELETE 并根据我们的需要以及处理表单请求所需的操作方法进行更改。
import { Form, useActionData } from '@remix-run/react';
export async function action({ request }) {
//handle logic with form data and return a value
}
export default function Index() {
const actionData = useActionData();
//we access the return value of the action with this hook
return (
<Form
method="post">
//add our form fields here
<button type="submit">Create Account</button>
</Form>
);
}
进入全屏模式 退出全屏模式
我们正在使用内置的 Remix 表单组件并使用 useActionData 挂钩。这是一个特殊的钩子,它将帮助我们使用 fetchAPI 将带有表单数据的请求(在本例中为 POST)发送到服务器。这将返回来自路由操作的 JSON 解析数据。它在以后处理表单验证错误时最常用。
添加我们的表格
我们可以使用从 Remix 导入的 Form 并在我们的 Form 中使用它。看看下面的代码片段有多简单
<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-lg w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Remix Form Validation with Zod
</h2>
</div>
<Form method="post" noValidate={true}>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700 pb-2"
>
Full name
</label>
<input
name="name"
type="text"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
<span className="text-sm text-red-500">
{/* print errors here */}
</span>
</div>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="Email"
className="block text-sm font-medium text-gray-700 pb-2"
>
Email
</label>
<input
name="email"
type="text"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
<span className="text-sm text-red-500">
{/* print errors here */}
</span>
</div>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="omfirm Email"
className="block text-sm font-medium text-gray-700 pb-2"
>
Confirm Email
</label>
<input
name="confirmEmail"
type="email"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
</div>
<span className="text-sm text-red-500">
{/* print errors here */}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="Expertise"
className="block text-sm font-medium text-gray-700"
>
Expertise
</label>
<select
name="expertise"
className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option></option>
<option>Product Designer</option>
<option>Frontend Developer</option>
<option>Backend Developer</option>
<option>Fullstack Developer</option>
</select>
</div>
<span className="text-sm text-red-500">
{/* print errors here */}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700 pb-2"
>
Github URL
</label>
<input
name="url"
type="text"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
</div>
<span className="text-sm text-red-500">
{/* print errors here */}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700"
>
Currently Available
</label>
<select
name="availability"
className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
>
<option></option>
<option>Full-time</option>
<option>Part-time</option>
<option>Contract</option>
<option>Freelance</option>
</select>
</div>
<span className="text-sm text-red-500">
{/* print errors here */}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700 pb-2"
>
Description
</label>
<textarea
name="description"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
</div>
<span className="text-sm text-red-500">
{/* print errors here */}
</span>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Submit
</button>
</div>
</Form>
</div>
</div>
进入全屏模式 退出全屏模式
我们有一个基本的表单结构,我们还挂钩了使用本机提交 formData() API 的提交按钮。
添加验证逻辑(带Zod)
当用户点击提交按钮时。动作函数被调用。这是我们将添加执行所需验证所需的所有逻辑的地方。
让我们先安装我们的库,然后才能使用它
npm i zod
进入全屏模式 退出全屏模式
import { ActionFunction } from '@remix-run/node';
import { z } from 'zod';
export const action: ActionFunction = async ({ request }) => {
const formPayload = Object.fromEntries(await request.formData());
const validationSchema = z
.object({
name: z.string().min(3),
email: z.string().email(),
confirmEmail: z.string().email(),
expertise: z.enum([
'Product Designer',
'Frontend Developer',
'Backend Developer',
'Fullstack Developer',
]),
url: z.string().url().optional(),
availability: z.enum(['Full-time', 'Part-time', 'Contract', 'Freelance']),
description: z.string().nullable(),
})
.refine((data) => data.email === data.confirmEmail, {
message: 'Email and confirmEmail should be same email',
path: ['confirmEmail'],
});
try {
const validatedSchema = validationSchema.parse(formPayload);
console.log('Form data is valid for submission:', validatedSchema); //API call can be made here
} catch (error) {
return {
formPayload,
error,
};
}
return {} as any;
};
进入全屏模式 退出全屏模式
验证逻辑中发生了几件事。我们在这里使用 Zod 提供给我们的 z.object({}) 方法定义了我们的模式。在给定的键中,我们根据需要添加验证逻辑。
您可能已经注意到我们涵盖了广泛的验证,其中仅包含字符串验证、电子邮件、最小字符、使用枚举、url、可选字段或可为空。后来我们还使用了 .refine 模式方法,它帮助我们通过细化添加自定义验证逻辑。
.refine(validator: (data:T)=>any, params?: RefineParams)
有了这个,我们可以在任何 Zod 模式中定义自定义验证检查。我们检查了两个电子邮件字段需要相互匹配的地方。您可以在此处的 Zod 文档中找到有关此方法的更多信息。
我们将继续在表单字段中添加其他属性,例如 key 和 defaultValue。在表单字段中使用 keyu003d{}。这是强制 React 重新渲染组件的陷阱。否则,您的数据可能不会更新。发生这种情况是因为当使用 defaultValueu003d{} 创建一个不受控制的组件时,React 将假定数据是不可变的,并且不会在值更改时重新渲染组件。
现在我们的表单标记看起来像这样
export default function Index() {
const actionData = useActionData();
return (
<div>
<div className="min-h-full flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-lg w-full space-y-8">
<div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Remix Form Validation with Zod
</h2>
</div>
<Form method="post" noValidate={true}>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700 pb-2"
>
Full name
</label>
<input
name="name"
type="text"
defaultValue={actionData?.formPayload?.name}
key={actionData?.formPayload?.name}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
<span className="text-sm text-red-500">
{actionData?.error?.issues[0]?.message}
</span>
</div>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="Email"
className="block text-sm font-medium text-gray-700 pb-2"
>
Email
</label>
<input
name="email"
type="text"
defaultValue={actionData?.formPayload?.email}
key={actionData?.formPayload?.email}
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
/>
<span className="text-sm text-red-500">
{actionData?.error?.issues[1]?.message}
</span>
</div>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="Confirm Email"
className="block text-sm font-medium text-gray-700 pb-2"
>
Confirm Email
</label>
<input
name="confirmEmail"
type="email"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
defaultValue={actionData?.formPayload?.confirmEmail}
key={actionData?.formPayload?.confirmEmail}
/>
</div>
<span className="text-sm text-red-500">
{actionData?.error?.issues[2]?.message}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="Expertise"
className="block text-sm font-medium text-gray-700"
>
Expertise
</label>
<select
name="expertise"
className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
defaultValue={actionData?.formPayload?.expertise}
key={actionData?.formPayload?.expertise}
>
<option></option>
<option>Product Designer</option>
<option>Frontend Developer</option>
<option>Backend Developer</option>
<option>Fullstack Developer</option>
</select>
</div>
<span className="text-sm text-red-500">
{actionData?.error?.issues[3]?.message}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700 pb-2"
>
Github URL
</label>
<input
name="url"
type="text"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
defaultValue={actionData?.formPayload?.url}
key={actionData?.formPayload?.url}
/>
</div>
<span className="text-sm text-red-500">
{actionData?.error?.issues[4]?.message}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700"
>
Currently Available
</label>
<select
name="availability"
className="mt-1 block w-full py-2 px-4 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
defaultValue={actionData?.formPayload?.availability}
key={actionData?.formPayload?.availability}
>
<option></option>
<option>Full-time</option>
<option>Part-time</option>
<option>Contract</option>
<option>Freelance</option>
</select>
</div>
<span className="text-sm text-red-500">
{actionData?.error?.issues[5]?.message}
</span>
</div>
<div className="rounded-md shadow-sm -space-y-px">
<div className="mb-6">
<label
htmlFor="company-website"
className="block text-sm font-medium text-gray-700 pb-2"
>
Description
</label>
<textarea
name="description"
className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
placeholder=""
defaultValue={actionData?.formPayload?.description}
key={actionData?.formPayload?.description}
/>
</div>
<span className="text-sm text-red-500">
{actionData?.error?.issues[6]?.message}
</span>
</div>
<div>
<button
type="submit"
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Submit
</button>
</div>
</Form>
</div>
</div>
</div>
);
}
进入全屏模式 退出全屏模式
结论
我们已经成功实现了表单验证。但需要注意的是,我们刚刚完成了服务器端验证,但客户端仍然存在。最好在客户端和服务器上都进行验证,这样就可以像我们期望的那样从用户那里获取数据。我们将在下一篇文章中设置它。
您可以在Github Repository中找到本文使用的源代码。
快乐编码!
更多推荐

所有评论(0)