A/B 测试设计与分析时使用。适用于产品实验、功能验证、转化率优化。融合假设驱动 + 样本量计算 + 置信度判断。
日本語の概要は準備中です。原文の説明を表示しています。
实现表单 / 校验 / 提交 / 错误处理时使用。覆盖 React Hook Form + Zod / TanStack Form / Formik / VeeValidate / Formily / Angular Forms。融合受控/非受控 + Schema 校验 + 异步校验 + 防重复提交。
インストール方法を見るインストールする前に、エージェントに与えられる指示の中身を確認できます。
参考来源:React Hook Form 官方、Zod 官方、VeeValidate 官方、Form Design Patterns(Adam Silver)。
1. Schema-first
定义 Zod / Yup schema → 自动生成类型 + 校验
2. 表单库性能优于 useState
useState 表单 → 每字段输入全表单重渲染
RHF / VeeValidate → 局部更新
3. 校验时机
- onChange:实时(性能差)
- onBlur:失焦(推荐)
- onSubmit:提交时
4. 错误信息友好
不写"VALIDATION_FAILED",写"邮箱格式不正确"
5. 防重复提交
isSubmitting 期间禁用按钮
或:去重 / 幂等键
6. 后端校验是兜底
前端校验只是体验,必须后端再校验
7. 字段级 vs 表单级错误
字段级:邮箱格式
表单级:服务器返回的"用户名已存在"
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
email: z.string().email('请输入有效邮箱'),
password: z.string().min(8, '密码至少 8 个字符').max(64, '密码不超过 64 个字符'),
remember: z.boolean().optional(),
});
type FormData = z.infer<typeof formSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting, isValid },
setError,
} = useForm<FormData>({
resolver: zodResolver(formSchema),
mode: 'onBlur', // 失焦校验
defaultValues: { email: '', password: '', remember: false },
});
const onSubmit = async (data: FormData) => {
try {
await api.login(data);
navigate('/dashboard');
} catch (err) {
if (err.code === 'INVALID_CREDENTIALS') {
setError('root', { message: '邮箱或密码错误' });
} else {
setError('root', { message: '登录失败,请稍后重试' });
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<div>
<label htmlFor="email">邮箱</label>
<input id="email" type="email" {...register('email')} aria-invalid={!!errors.email} />
{errors.email && <p role="alert">{errors.email.message}</p>}
</div>
<div>
<label htmlFor="password">密码</label>
<input id="password" type="password" {...register('password')} aria-invalid={!!errors.password} />
{errors.password && <p role="alert">{errors.password.message}</p>}
</div>
<label>
<input type="checkbox" {...register('remember')} />
记住我
</label>
{errors.root && <p role="alert">{errors.root.message}</p>}
<button type="submit" disabled={isSubmitting || !isValid}>
{isSubmitting ? '登录中...' : '登录'}
</button>
</form>
);
}
const registerSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: '密码不一致',
path: ['confirmPassword'],
})
.refine(
async (data) => {
const res = await api.checkUsernameAvailable(data.username);
return res.available;
},
{
message: '用户名已被占用',
path: ['username'],
}
);
import { useFieldArray } from 'react-hook-form';
const orderSchema = z.object({
customer: z.string().min(1),
items: z.array(
z.object({
productId: z.number().int().positive(),
quantity: z.number().int().positive().max(100),
})
).min(1, '至少 1 个商品'),
});
function OrderForm() {
const { control, register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(orderSchema),
defaultValues: { customer: '', items: [{ productId: 0, quantity: 1 }] },
});
const { fields, append, remove } = useFieldArray({ control, name: 'items' });
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('customer')} />
{fields.map((field, index) => (
<div key={field.id}>
<input type="number" {...register(`items.${index}.productId`)} />
<input type="number" {...register(`items.${index}.quantity`)} />
<button type="button" onClick={() => remove(index)}>删除</button>
</div>
))}
<button type="button" onClick={() => append({ productId: 0, quantity: 1 })}>
添加商品
</button>
<button type="submit">提交</button>
</form>
);
}
function MultiStepForm() {
const [step, setStep] = useState(0);
const methods = useForm<FullFormData>({ resolver: zodResolver(fullSchema) });
const stepSchemas = [stepOneSchema, stepTwoSchema, stepThreeSchema];
const next = async () => {
const valid = await methods.trigger(stepFields[step]); // 仅校验当前步
if (valid) setStep(s => s + 1);
};
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)}>
{step === 0 && <StepOne />}
{step === 1 && <StepTwo />}
{step === 2 && <StepThree />}
<div>
{step > 0 && <button onClick={() => setStep(s => s - 1)}>上一步</button>}
{step < 2 && <button type="button" onClick={next}>下一步</button>}
{step === 2 && <button type="submit">提交</button>}
</div>
</form>
</FormProvider>
);
}
const fileSchema = z.object({
avatar: z.instanceof(FileList)
.refine(files => files.length > 0, '请选择文件')
.refine(files => files[0]?.size <= 5 * 1024 * 1024, '文件不超过 5MB')
.refine(files => ['image/jpeg', 'image/png'].includes(files[0]?.type), '仅支持 JPG/PNG'),
});
function UploadForm() {
const { register, handleSubmit, watch } = useForm({ resolver: zodResolver(fileSchema) });
const file = watch('avatar')?.[0];
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="file" accept="image/*" {...register('avatar')} />
{file && <img src={URL.createObjectURL(file)} alt="预览" />}
<button type="submit">上传</button>
</form>
);
}
import { useForm } from '@tanstack/react-form';
function MyForm() {
const form = useForm({
defaultValues: { email: '', password: '' },
onSubmit: async ({ value }) => {
await api.login(value);
},
validators: {
onChange: ({ value }) => formSchema.safeParse(value).error,
},
});
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.Field
name="email"
validators={{
onBlur: z.string().email('请输入邮箱'),
}}
children={(field) => (
<>
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
/>
{field.state.meta.errors.length > 0 && <span>{field.state.meta.errors[0]}</span>}
</>
)}
/>
</form>
);
}
<script setup lang="ts">
import { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';
const schema = toTypedSchema(z.object({
email: z.string().email('请输入邮箱'),
password: z.string().min(8, '密码至少 8 个字符'),
}));
const { defineField, handleSubmit, errors, isSubmitting } = useForm({
validationSchema: schema,
});
const [email, emailAttrs] = defineField('email');
const [password, passwordAttrs] = defineField('password');
const onSubmit = handleSubmit(async (values) => {
await api.login(values);
});
</script>
<template>
<form @submit="onSubmit" novalidate>
<input v-model="email" v-bind="emailAttrs" type="email" />
<p v-if="errors.email">{{ errors.email }}</p>
<input v-model="password" v-bind="passwordAttrs" type="password" />
<p v-if="errors.password">{{ errors.password }}</p>
<button :disabled="isSubmitting">登录</button>
</form>
</template>
// 适合:动态表单、JSON Schema 驱动、可视化表单设计器
import { createForm } from '@formily/core';
import { FormProvider, FormItem, Input, Submit } from '@formily/antdv';
const form = createForm();
const schema = {
type: 'object',
properties: {
email: {
type: 'string',
title: '邮箱',
required: true,
'x-validator': [{ format: 'email' }],
'x-component': 'Input',
},
},
};
@Component({
selector: 'app-login',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input formControlName="email" type="email" />
@if (form.get('email')?.errors?.['email']) {
<p>请输入邮箱</p>
}
<input formControlName="password" type="password" />
<button [disabled]="form.invalid || submitting()">登录</button>
</form>
`,
})
export class LoginComponent {
fb = inject(FormBuilder);
submitting = signal(false);
form = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
async onSubmit() {
if (this.form.invalid) return;
this.submitting.set(true);
try {
await this.authService.login(this.form.value);
} finally {
this.submitting.set(false);
}
}
}
<!-- routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<form method="POST" use:enhance>
<input name="email" type="email" required />
{#if form?.errors?.email}<p>{form.errors.email}</p>{/if}
<input name="password" type="password" required />
<button>登录</button>
</form>
<!-- routes/login/+page.server.ts -->
<script lang="ts">
import { fail, redirect } from '@sveltejs/kit';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const actions = {
default: async ({ request }) => {
const formData = Object.fromEntries(await request.formData());
const result = schema.safeParse(formData);
if (!result.success) {
return fail(400, { errors: result.error.flatten().fieldErrors });
}
try {
await login(result.data);
} catch {
return fail(401, { message: '邮箱或密码错误' });
}
throw redirect(303, '/dashboard');
},
};
</script>
| 库 | 大小 | TS | 性能 | 推荐 |
|---|---|---|---|---|
| Zod | 11KB | 强 | 中 | ⭐⭐⭐⭐⭐ |
| Yup | 28KB | 中 | 中 | ⭐⭐⭐ |
| Valibot | 1KB | 强 | 高 | ⭐⭐⭐⭐⭐ |
| Joi | 145KB | 弱 | 中 | ⭐⭐ |
| ArkType | - | 极强 | 高 | ⭐⭐⭐⭐ |
onChange(每字符):
✅ 适合:密码强度提示、计数
❌ 不适合:异步校验(频繁请求)、长表单
onBlur(失焦):
✅ 适合:通用校验、邮箱格式
❌ 不适合:实时反馈
onSubmit(提交时):
✅ 适合:简单表单、确认操作
❌ 不适合:需要立即反馈
混合(推荐):
- 第一次提交后切到 onChange
- 默认 onBlur
// React Hook Form
const { handleSubmit, formState: { isSubmitting } } = useForm();
// 自动 isSubmitting,按钮 disabled
<button disabled={isSubmitting}>提交</button>
// 自定义 + 节流
const onSubmit = useCallback(
throttle(async (data) => {
await api.submit(data);
}, 1000),
[]
);
// 幂等键(重要操作)
const idempotencyKey = useMemo(() => crypto.randomUUID(), []);
const onSubmit = async (data) => {
await api.createOrder(data, { headers: { 'Idempotency-Key': idempotencyKey } });
};
// LocalStorage 自动保存
function useDraft<T>(formId: string, watch: () => T) {
const [restored, setRestored] = useState(false);
const data = watch();
useEffect(() => {
const saved = localStorage.getItem(`draft:${formId}`);
if (saved && !restored) {
// 用 form.reset(JSON.parse(saved)) 恢复
setRestored(true);
}
}, [formId]);
useEffect(() => {
if (restored) {
localStorage.setItem(`draft:${formId}`, JSON.stringify(data));
}
}, [data, restored, formId]);
const clearDraft = () => localStorage.removeItem(`draft:${formId}`);
return { clearDraft };
}
1. 设计表单 schema(Zod)
- 字段 + 校验规则
2. 选择表单库(RHF / TanStack Form / VeeValidate)
3. 实现表单
- 受控 vs 非受控
- 校验时机
- 错误展示
4. 防重复提交(isSubmitting / 幂等键)
5. 处理提交错误
- 字段级 vs 表单级
- 业务错误友好提示
6. 草稿保存(如长表单)
7. 测试
- 校验规则
- 提交成功 / 失败
- 防重复
templates/forms-checklist.md — 表单清单 + 校验规则 + 错误处理 + 测试□ Schema-first(Zod / Valibot)
□ 类型自动派生
□ 受控 / 非受控明确
□ 校验时机合理
□ 错误信息友好(用户语言)
□ 防重复提交
□ 异步校验防抖
□ Loading / Error 状态
□ 必填用 *
□ 错误用 aria-invalid
□ 标签关联(label htmlFor)
□ 焦点管理(首个错误聚焦)
□ 键盘可操作
□ 移动端友好(type / inputmode)
□ 草稿保存(如长表单)
□ 后端校验兜底
上游:
ui-ux-designer 工作流 → 表单设计 / 状态
api-designer → 字段约束 / 错误码
下游:
data-fetching → 提交 mutation
state-management → 表单状态
accessibility-implementation → ARIA / 焦点
testing-frontend → 表单测试
references/frontend-tech-stack-guide.md — 表单库まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
A/B 测试设计与分析时使用。适用于产品实验、功能验证、转化率优化。融合假设驱动 + 样本量计算 + 置信度判断。
日本語の概要は準備中です。原文の説明を表示しています。
上线前业务方/产品方验收时使用。适用于功能交付确认、UAT、发布会签。融合 BDD Given-When-Then、Specification by Example、ATDD。
日本語の概要は準備中です。原文の説明を表示しています。
设计无障碍可访问的界面时使用。适用于满足 WCAG 标准、键盘导航、屏幕阅读器支持。优先使用 WCAG 2.2 AA 级标准 + 键盘 + 焦点 + 对比度 + ARIA。
日本語の概要は準備中です。原文の説明を表示しています。
实现可访问性(无障碍)时使用。覆盖 WCAG 2.2 AA / ARIA / 键盘导航 / 屏幕阅读器 / 颜色对比 / 焦点管理。融合 React Aria / Radix UI / Headless UI 实践 + axe-core 自动化测试。
日本語の概要は準備中です。原文の説明を表示しています。
AI 模型部署时使用。适用于推理服务 / GPU / TensorRT / 监控 / 扩缩容。
日本語の概要は準備中です。原文の説明を表示しています。
数据分析报告输出时使用。适用于业务分析报告、周报、专项分析、决策支持。融合洞察 + 建议 + 行动项。
日本語の概要は準備中です。原文の説明を表示しています。