01MVP 标识01MVP
包文档认证与配置Validators

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/resolvers

3. 在项目中使用

// 客户端组件
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