Validators
集中式 Zod 验证模式,支持国际化错误消息
@mono/validators
集中式 Zod 验证模式,支持国际化错误消息。提供类型安全的表单验证,统一的错误处理,以及开箱即用的常用验证规则。
特性
- 类型安全 - 基于 Zod 的完整 TypeScript 类型推导
- 国际化支持 - 集成 next-intl,支持多语言错误消息
- 常用模式 - 预定义的邮箱、密码、手机号、用户名验证
- 认证表单 - 注册、登录、重置密码等完整表单验证
- 用户管理 - 用户资料更新验证
- 可扩展 - 轻松创建自定义验证模式
安装
pnpm add @mono/validators快速开始
基础用法
import { createValidators } from '@mono/validators';
import { useTranslations } from 'next-intl';
// 在组件中使用
const t = useTranslations();
const validators = createValidators(t);
// 验证邮箱
const result = validators.email.safeParse('test@example.com');
if (!result.success) {
console.error(result.error.errors);
}
// 验证注册表单
const signupResult = validators.signupEmail.safeParse({
email: 'user@example.com',
password: 'Password123'
});在表单中使用
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createValidators } from '@mono/validators';
import { useTranslations } from 'next-intl';
export function SignupForm() {
const t = useTranslations();
const validators = createValidators(t);
const form = useForm({
resolver: zodResolver(validators.signupEmail),
defaultValues: {
email: '',
password: ''
}
});
const onSubmit = async (data) => {
// 数据已经通过验证
console.log('Valid data:', data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* 表单字段 */}
</form>
);
}API 参考
createValidators(t)
创建带有国际化支持的验证器模式。
参数:
t: 翻译函数(key: string, params?: Record<string, any>) => string
返回: 包含所有验证器模式的对象
可用模式
通用验证
-
email- 邮箱验证validators.email.parse('user@example.com'); -
password- 密码验证(最少 8 位,包含大小写字母和数字)validators.password.parse('Password123'); -
phone- 手机号验证validators.phone.parse('+8613800138000'); -
username- 用户名验证(3-30 字符,字母数字 + _ -)validators.username.parse('john_doe');
认证表单
-
signupEmail- 邮箱注册表单validators.signupEmail.parse({ email: 'user@example.com', password: 'Password123' }); -
signupPhone- 手机号注册表单validators.signupPhone.parse({ phone: '+8613800138000', code: '123456' }); -
loginEmail- 邮箱登录表单validators.loginEmail.parse({ email: 'user@example.com', password: 'Password123' }); -
loginPhone- 手机号登录表单validators.loginPhone.parse({ phone: '+8613800138000', code: '123456' }); -
resetPassword- 密码重置表单validators.resetPassword.parse({ email: 'user@example.com' });
用户管理
updateProfile- 用户资料更新表单validators.updateProfile.parse({ name: 'John Doe', username: 'john_doe' });
实战示例
示例 1: 注册表单验证
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createValidators } from '@mono/validators';
import { useTranslations } from 'next-intl';
import { Button, Input, Form } from '@mono/ui';
export function SignupForm() {
const t = useTranslations();
const validators = createValidators(t);
const form = useForm({
resolver: zodResolver(validators.signupEmail),
defaultValues: {
email: '',
password: ''
}
});
const onSubmit = async (data) => {
try {
const response = await fetch('/api/auth/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error('Signup failed');
}
// 注册成功,跳转到首页
window.location.href = '/';
} catch (error) {
console.error('Signup error:', error);
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Form.Field
control={form.control}
name="email"
render={({ field }) => (
<Form.Item>
<Form.Label>邮箱</Form.Label>
<Form.Control>
<Input {...field} type="email" placeholder="your@email.com" />
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
<Form.Field
control={form.control}
name="password"
render={({ field }) => (
<Form.Item>
<Form.Label>密码</Form.Label>
<Form.Control>
<Input {...field} type="password" placeholder="••••••••" />
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
<Button type="submit" className="w-full">
注册
</Button>
</form>
</Form>
);
}示例 2: API 路由验证
// app/api/auth/signup/route.ts
import { createValidators } from '@mono/validators';
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
try {
// 创建验证器(服务端可以使用简单的翻译函数)
const t = (key: string) => key;
const validators = createValidators(t);
// 解析请求体
const body = await req.json();
// 验证数据
const result = validators.signupEmail.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ errors: result.error.errors },
{ status: 400 }
);
}
// 数据有效,处理注册逻辑
const { email, password } = result.data;
// ... 创建用户
return NextResponse.json({ success: true });
} catch (error) {
console.error('Signup error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}示例 3: 手机号登录
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createValidators } from '@mono/validators';
import { useTranslations } from 'next-intl';
import { Button, Input, Form } from '@mono/ui';
export function PhoneLoginForm() {
const t = useTranslations();
const validators = createValidators(t);
const [codeSent, setCodeSent] = useState(false);
const form = useForm({
resolver: zodResolver(validators.loginPhone),
defaultValues: {
phone: '',
code: ''
}
});
const sendCode = async () => {
const phone = form.getValues('phone');
// 验证手机号
const result = validators.phone.safeParse(phone);
if (!result.success) {
form.setError('phone', {
message: result.error.errors[0].message
});
return;
}
// 发送验证码
await fetch('/api/auth/send-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone })
});
setCodeSent(true);
};
const onSubmit = async (data) => {
// 提交登录
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
window.location.href = '/';
}
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Form.Field
control={form.control}
name="phone"
render={({ field }) => (
<Form.Item>
<Form.Label>手机号</Form.Label>
<div className="flex gap-2">
<Form.Control>
<Input {...field} placeholder="+86 138 0013 8000" />
</Form.Control>
<Button
type="button"
variant="outline"
onClick={sendCode}
disabled={codeSent}
>
{codeSent ? '已发送' : '发送验证码'}
</Button>
</div>
<Form.Message />
</Form.Item>
)}
/>
<Form.Field
control={form.control}
name="code"
render={({ field }) => (
<Form.Item>
<Form.Label>验证码</Form.Label>
<Form.Control>
<Input {...field} placeholder="123456" maxLength={6} />
</Form.Control>
<Form.Message />
</Form.Item>
)}
/>
<Button type="submit" className="w-full">
登录
</Button>
</form>
</Form>
);
}示例 4: 自定义验证模式
import { z } from 'zod';
import { createValidators } from '@mono/validators';
import { useTranslations } from 'next-intl';
export function useCustomValidators() {
const t = useTranslations();
const baseValidators = createValidators(t);
// 扩展基础验证器
const customValidators = {
...baseValidators,
// 自定义:公司注册表单
companySignup: z.object({
companyName: z.string().min(2, t('validation.company.min', { min: 2 })),
email: baseValidators.email,
password: baseValidators.password,
industry: z.enum(['tech', 'finance', 'retail', 'other']),
size: z.enum(['1-10', '11-50', '51-200', '200+']),
}),
// 自定义:邀请码验证
inviteCode: z.string()
.length(8, t('validation.inviteCode.length'))
.regex(/^[A-Z0-9]+$/, t('validation.inviteCode.format')),
};
return customValidators;
}示例 5: 多步骤表单验证
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { createValidators } from '@mono/validators';
import { useTranslations } from 'next-intl';
export function MultiStepSignupForm() {
const t = useTranslations();
const validators = createValidators(t);
const [step, setStep] = useState(1);
// 第一步:基本信息
const step1Schema = z.object({
email: validators.email,
password: validators.password,
});
// 第二步:个人信息
const step2Schema = z.object({
name: z.string().min(2),
username: validators.username,
});
// 第三步:偏好设置
const step3Schema = z.object({
newsletter: z.boolean(),
terms: z.boolean().refine(val => val === true, {
message: t('validation.terms.required')
}),
});
// 完整表单模式
const fullSchema = step1Schema.merge(step2Schema).merge(step3Schema);
const form = useForm({
resolver: zodResolver(
step === 1 ? step1Schema :
step === 2 ? step2Schema :
step3Schema
),
defaultValues: {
email: '',
password: '',
name: '',
username: '',
newsletter: false,
terms: false,
}
});
const onNext = async () => {
const isValid = await form.trigger();
if (isValid) {
setStep(step + 1);
}
};
const onSubmit = async (data) => {
// 最终提交
console.log('Complete data:', data);
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{step === 1 && (
<div>
{/* 第一步表单字段 */}
<Button type="button" onClick={onNext}>下一步</Button>
</div>
)}
{step === 2 && (
<div>
{/* 第二步表单字段 */}
<Button type="button" onClick={() => setStep(1)}>上一步</Button>
<Button type="button" onClick={onNext}>下一步</Button>
</div>
)}
{step === 3 && (
<div>
{/* 第三步表单字段 */}
<Button type="button" onClick={() => setStep(2)}>上一步</Button>
<Button type="submit">完成注册</Button>
</div>
)}
</form>
);
}示例 6: 服务端组件验证
// app/profile/edit/page.tsx
import { createValidators } from '@mono/validators';
import { redirect } from 'next/navigation';
import { revalidatePath } from 'next/cache';
export default function EditProfilePage() {
async function updateProfile(formData: FormData) {
'use server';
const t = (key: string) => key;
const validators = createValidators(t);
const data = {
name: formData.get('name'),
username: formData.get('username'),
};
const result = validators.updateProfile.safeParse(data);
if (!result.success) {
// 处理验证错误
return { errors: result.error.errors };
}
// 更新用户资料
// await updateUserProfile(result.data);
revalidatePath('/profile');
redirect('/profile');
}
return (
<form action={updateProfile}>
<input name="name" placeholder="Name" />
<input name="username" placeholder="Username" />
<button type="submit">保存</button>
</form>
);
}集成指南
1. 添加翻译键
在你的 i18n 消息文件中添加以下键:
{
validation: {
email: {
required: '邮箱是必填项',
invalid: '邮箱格式不正确'
},
password: {
min: '密码至少需要 {min} 个字符',
max: '密码最多 {max} 个字符',
strength: '密码必须包含大小写字母和数字'
},
phone: {
required: '手机号是必填项',
invalid: '手机号格式不正确'
},
username: {
min: '用户名至少需要 {min} 个字符',
max: '用户名最多 {max} 个字符',
invalid: '用户名只能包含字母、数字、下划线和连字符'
},
code: {
required: '验证码是必填项'
}
}
}2. 配置 React Hook Form
pnpm add react-hook-form @hookform/resolvers3. 在项目中使用
// 客户端组件
import { createValidators } from '@mono/validators';
import { useTranslations } from 'next-intl';
const t = useTranslations();
const validators = createValidators(t);
// 服务端
const t = (key: string) => key; // 或使用服务端 i18n
const validators = createValidators(t);最佳实践
1. 统一验证逻辑
在客户端和服务端使用相同的验证模式:
// shared/validators.ts
import { createValidators } from '@mono/validators';
export function getValidators(t: (key: string) => string) {
return createValidators(t);
}
// 客户端
const validators = getValidators(useTranslations());
// 服务端
const validators = getValidators((key) => key);2. 类型安全
利用 Zod 的类型推导:
import { z } from 'zod';
import { createValidators } from '@mono/validators';
const validators = createValidators(t);
// 自动推导类型
type SignupData = z.infer<typeof validators.signupEmail>;
// { email: string; password: string }3. 错误处理
统一处理验证错误:
function handleValidationError(error: z.ZodError) {
const fieldErrors: Record<string, string> = {};
error.errors.forEach((err) => {
const path = err.path.join('.');
fieldErrors[path] = err.message;
});
return fieldErrors;
}4. 自定义验证规则
扩展基础验证器:
const customValidators = {
...createValidators(t),
customField: z.string()
.min(5)
.refine(
(val) => !val.includes('spam'),
{ message: '不允许包含敏感词' }
),
};故障排查
问题:翻译键未找到
解决方案: 确保在 i18n 消息文件中添加了所有必需的翻译键。
问题:验证在服务端失败
解决方案: 服务端需要提供翻译函数,��使是简单的 (key) => key 也可以。
问题:类型错误
解决方案: 确保安装了 zod 和 @hookform/resolvers 的类型定义。
相关文档
许可证
MIT