返回探索
指南VibeFix 编辑部更新于 2026年10月16日

别让用户猜哪里填错了:vibe 项目的表单验证与提交实战

每个 vibe 项目都有一堆表单,AI 生成的只会堆 required——模糊的文案、异步竞态、重复提交、丢失的草稿才是用户流失的地方。本文给一人团队完整的表单实战:校验三层分工、错误文案改写、防抖防竞态的异步校验、idle/submitting/success/error 提交状态机与防重提交三保险、文件型表单、多步表单草稿、无障碍,以及上线前 12 项检查表。

网页表单截图:字段下方显示内联验证错误信息

划界:本篇讲的是通用表单的方法论,与站内三篇明确分工:auth-implementation-vibe-projects-guide(鉴权落地实战)里的登录/注册表单只是表单的一种,本篇讲的是"所有表单通用的验证与提交模式"——登录表单是本篇模式的一个实例;vibe-project-user-feedback-loop-guide(用户反馈闭环)第 3 节的"30 秒反馈表单"是"反馈场景的表单设计",本篇是"表单本身的工程实战"——场景 vs 机制;vibe-project-file-upload-guide(文件上传)讲的是"文件怎么传",本篇第 5 节讲的是"带文件的表单整体怎么做",上传链路直接复用它。

注册、结算、设置、工单——每个 vibe 项目都有一堆表单。AI 生成的表单长一个样:每个 input 后面跟一个 required,提交时浏览器弹一句"请填写此字段",完事。然后真实用户来了:手机号输了 10 位,报错只说"无效输入",他不知道错在哪;用户名输完发现已被占用,是在点了提交之后;网络卡了一下连点三次提交,生成了三笔订单;切到下一步再回来,前面填的全没了。表单是 vibe 项目里"离钱最近"的组件——注册流掉一个人就是少一个用户,结算页多一次报错就是少一笔订单。本文讲一人团队今天就能照着做的表单实战:校验分几层、错误文案怎么写、异步校验怎么防抖防竞态、提交状态机怎么画、文件和多步表单怎么处理、无障碍怎么做、上线前查什么。

一、校验三层:即时、失焦、提交前,各管各的

AI 最爱生成的写法是"全用 onChange 校验":用户每敲一个字母就全量跑一遍校验,刚输完"1"就报"手机号应为 11 位"。这种体验叫"审判式表单"——用户还没写完,判决先下来了。正确的分工是三层,各管各的:

  • 第 1 层:输入时(onChange)——只做"格式引导",不判错。比如手机号输入框,输入时只做一件事:自动过滤非数字、按 3-4-4 分组显示。这一层不显示红色错误,它的目标是"让用户不容易输错",而不是"指出用户输错了"。
  • 第 2 层:失焦时(onBlur)——判"这个字段"的错。用户离开字段,说明他自认为填完了,这时给反馈最合适。错误信息出现在字段正下方,红色,但语气是帮助不是审判。
  • 第 3 层:提交前——全量校验,一次性告诉用户所有问题。这是最后一道门:跑全部规则,拦下所有不合格的提交,并把焦点送到第一个错误字段(第 7 节)。

三层分工表:

  • onChange:格式化输入(数字分组、大小写转换、去空格)→ 不报错
  • onBlur:单字段校验(格式、长度、必填)→ 字段下报错
  • onSubmit:全量校验(含跨字段规则,如"确认密码一致")→ 顶部汇总 + 字段下报错 + 焦点跳转

用 React Hook Form 落地时,对应的是 mode 配置:mode: "onBlur" 让失焦校验成为默认,提交时 handleSubmit 自动跑全量。不要设 mode: "onChange" 全局开启——那就是 AI 默认给你挖的坑。截至 2026-10-16 亲核的公开资料:2026 年 9 月初的 npm latest 记录为 react-hook-form 7.87.0、@hookform/resolvers 5.9.1、zod 4.5.4,RHF 保持 v7 大版本、zod 已到 v4;是否用 zod 做 schema,以你项目的实际依赖为准,具体版本号以 npm 为准。

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

// schema 即文档:字段规则一处定义,前后端可复用同一份
const schema = z.object({
  phone: z.string().regex(/^1\d{10}$/, "手机号应为 11 位数字,以 1 开头"),
  password: z.string().min(8, "密码至少 8 位"),
  confirm: z.string(),
}).refine((d) => d.password === d.confirm, {
  message: "两次输入的密码不一致",
  path: ["confirm"], // 错误挂到 confirm 字段下
});

const { register, handleSubmit, formState: { errors } } = useForm({
  resolver: zodResolver(schema),
  mode: "onBlur", // 失焦校验为默认,提交时自动全量
});

二、错误文案写作:从"无效输入"到"告诉我怎么改"

错误文案只有一条金标准:用户看完知道"改什么、改成什么样"。"无效输入""格式错误""校验失败"这三句是 AI 的口头禅,全部不及格。改写对照:

  • ❌ "无效输入" → ✅ "手机号应为 11 位数字,你输了 10 位"
  • ❌ "格式错误" → ✅ "邮箱格式不对,检查一下 @ 后面是不是少了域名"
  • ❌ "密码太弱" → ✅ "密码至少 8 位,再加 1 个数字试试"
  • ❌ "提交失败" → ✅ "保存失败:网络超时,你填的内容还在,再点一次提交"

写文案的三条规则:

  • 说人话,不说术语。"正则校验未通过"是写给开发者的,"应为 11 位数字"是写给用户的。文案评审时找个非技术朋友读一遍,他皱眉就重写。
  • 给"改成什么样"。光说"不对"不够,要说"对的是什么样"。最好附上例子:"如 13812345678"。
  • 错误出现的位置:字段下为主,顶部汇总为辅。单字段错误紧贴字段下方(用户视线不用跳);提交时多个字段都有错,顶部再加一条汇总条"有 3 处需要修改",点击可跳到对应字段。只在顶部报错、让用户自己去找是哪个字段——这是最常见的反模式。

文案全部收敛到 schema 的 message 里(见第 1 节代码),不要散落在组件的 JSX 中——以后改一句文案只改一处,也方便统一做多语言。

三、异步校验实战:唯一性检查的防抖与竞态

用户名、邮箱、邀请码这类"是否可用"的校验必须问后端。AI 生成的版本通常是 onChange 里直接 fetch,三个坑一次踩全:请求打爆后端、后发的请求先回来(竞态)、用户不知道"正在检查"。

import { useEffect, useRef, useState } from "react";

function useUniqueCheck(value: string, check: (v: string) => Promise<boolean>) {
  const [state, setState] = useState<"idle" | "checking" | "ok" | "taken">("idle");
  const seq = useRef(0); // 竞态守卫:只认最新一次请求的结果
  useEffect(() => {
    if (!value) { setState("idle"); return; }
    setState("checking");
    const mySeq = ++seq.current;
    const timer = setTimeout(async () => {
      const available = await check(value);
      if (seq.current === mySeq) {           // 后发先至:过期响应直接丢弃
        setState(available ? "ok" : "taken");
      }
    }, 400); // 防抖 400ms:用户停手才发请求
    return () => clearTimeout(timer);
  }, [value]);
  return state;
}
// UI 上:checking 显示 spinner,taken 在字段下报"该用户名已被占用,试试 xxx_2026"

三条实战规则:

  • 防抖 300–500ms 再发请求。用户每敲一个字母就查一次,是对后端的 DDoS。400ms 是经验值:停手即查,不拖沓。
  • 竞态必须处理。网络抖动时"先发的请求后回来"极其常见。用递增序号(或 AbortController 取消旧请求)保证"只认最后一次输入的结果",否则用户会看到"可用"和"被占用"来回横跳。
  • pending 状态要可见。检查中显示小 spinner;更重要的是,提交按钮在异步校验 pending 时必须禁用——否则用户可能在"用户名是否可用"还没查完时就提交了。

四、提交状态机:idle → submitting → success / error

这是全文最重要的一张图。表单提交不是"调一下接口",而是一个状态机:

网页表单截图:字段下方显示内联验证错误信息网页表单截图:字段下方显示内联验证错误信息

四个状态各管一摊:

  • idle:按钮文案"立即提交",可点击。这是表单的常态。
  • submitting:按钮禁用,文案变成"提交中…"。禁用是防重提交的第一道门——95% 的连点在这里被挡住。
  • success:成功态。跳转、toast、清空表单三选一,不要停留在 submitting。
  • error:错误态。按钮恢复可点,文案变回"重新提交";关键动作:自动聚焦第一个错误字段(第 7 节),并在顶部给出一句话摘要。

防重提交要三保险,缺一不可(见上图底部):

const inFlight = useRef(false); // 第②保险:函数级守卫
async function onSubmit(data: FormData) {
  if (inFlight.current) return;   // 回车连击、程序化重复调用在这里被挡
  inFlight.current = true;
  try {
    await fetch("/api/orders", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        // 第③保险:幂等键——网络重试、双 tab 打开同一表单时,后端认键去重
        "Idempotency-Key": crypto.randomUUID(),
      },
      body: JSON.stringify(data),
    });
    // → success
  } catch {
    // → error:文案写清楚"内容还在,再点一次"
  } finally {
    inFlight.current = false;
  }
}

第③条是很多人漏掉的:按钮 disabled 挡得住连点,挡不住"用户开了两个 tab 同时提交"和"弱网下浏览器自动重试"。幂等键让后端对同一个键只处理一次,是防重提交的真正解法——代价是后端要存键→结果的映射,建议从订单、支付类接口先做起。

五、文件型表单:头像与附件的表单内联处理

带文件的表单(头像上传、工单附件、实名认证材料)多了一个维度:文件本身要先过检,再进表单。规则是前端预检 + 复用上传链路:

  • 大小、类型、数量在前端先拦。用户选了个 200MB 的视频,不要等到上传 3 分钟后才告诉他"太大了"。input 的 accept 属性 + JS 二次检查(file.size、file.type),秒级反馈。
  • 上传链路直接复用 vibe-project-file-upload-guide 的方案(直传对象存储、分片、断点续传),本篇不重复讲。本篇只讲表单侧:上传进度要显示在字段内(进度条),上传完成前提交按钮禁用——否则用户可能提交了一个"文件还没传完"的表单。
  • 失败可重试、单文件可删。某个附件上传失败,只重试那一个,不重置整个表单;已选文件列表支持单独删除。这是文件型表单和普通字段最大的区别:它的"值"是有生命周期的。
// 伪代码:文件字段的状态
type FileFieldState =
  | { status: "empty" }
  | { status: "checking" }            // 前端预检中(大小/类型)
  | { status: "uploading"; progress: number }
  | { status: "done"; url: string }    // 上传完成,url 进表单数据
  | { status: "error"; message: string }; // 可单独重试
// 提交守卫:所有文件字段 status === "done" 才允许提交

六、多步表单:分步校验与草稿自动保存

注册、入驻、问卷这类长表单要拆成多步。每一步是一个独立的子表单,规则如下:

  • 分步校验:点"下一步"时只校验当前步的字段,不校验还没填的后面的步骤。用户在第 1 步就被第 3 步的错误拦住,是最劝退的设计。用 schema 的 pick 拆分子 schema,每步一个。
  • 草稿自动保存:每步填完写一份 localStorage(key 带表单 id 和版本号)。用户刷新、误关、手机切出去回不来——回来时问一句"检测到未完成的草稿,是否继续?",转化率能差出一截。注意隐私:含身份证、银行卡的表单,草稿要加密或只存非敏感字段,并在提交成功后清除。
  • 返回上一步数据不丢:步骤状态(当前第几步 + 各步数据)放在表单级 state,不要卸载步骤组件就丢数据。最简单的实现:所有步骤的字段注册在同一个 useForm 里,步骤切换只切换"显示哪一组",数据天然保留。
  • 进度指示器:顶部明确"第 2 步 / 共 4 步",让用户有掌控感。允许点已完成的步骤回跳修改,回跳不清空数据。
// 步骤数据天然保留:一个 useForm 管所有步骤,切换只换显示
const methods = useForm({ resolver: zodResolver(fullSchema), mode: "onBlur" });
const [step, setStep] = useState(0);
const stepFields = [["name", "phone"], ["company", "role"], ["bio"]][step];

async function next() {
  const ok = await methods.trigger(stepFields[step] as any); // 只校验当前步
  if (ok) {
    saveDraft(methods.getValues()); // 过步即存草稿
    setStep((s) => s + 1);
  }
  // else:错误已在字段下显示,trigger 会自动聚焦第一个错误字段
}

七、无障碍:label、错误关联与焦点管理

表单是无障碍的重灾区,也是最容易拿分的——读屏用户填不了表单等于产品对他关闭。 checklist 很短,逐项落实:

  • 每个输入都有 label,且用 htmlFor/id 显式关联。placeholder 不是 label——用户一打字它就消失了,读屏软件也读不到稳定的名称。
  • 错误信息用 aria-describedby 指向。<input aria-describedby="phone-error"> + <p id="phone-error">,这样读屏用户聚焦到字段时能同时听到错误内容,而不是只听到"无效"。
  • 用 aria-invalid="true" 标记出错字段,配合视觉上的红色边框(不要只用颜色:色弱用户需要图标或文字辅助)。
  • 提交失败自动聚焦第一个错误字段。这是第 4 节状态机的 error 态的关键动作:一行代码 setFocus(firstErrorField)(RHF 自带),键盘用户和读屏用户立刻知道从哪改起。视觉上再滚动到该字段。
  • 必填用 required 属性 + 文字"必填"标注,不要只用红色星号——读屏软件对纯符号的播报不可靠。

八、上线检查表:12 项

  • 1. 三层校验分工正确:onChange 只做格式化不报错;onBlur 单字段报错;提交前全量。抽查三个字段走一遍。
  • 2. 错误文案评审:找非技术人员读一遍所有报错,他能说出"该怎么改"才算过。
  • 3. 异步校验三件套:防抖、竞态处理、pending 时禁用提交——逐项验证。
  • 4. 防重提交三保险:按钮 disabled、inFlight 守卫、幂等键——连点 10 次只产生一笔。
  • 5. 弱网测试:Network 面板开 Slow 3G 走一遍提交,按钮文案、禁用、超时报错都要对。
  • 6. 文件型表单:超大文件秒级拦截、上传中禁用提交、单文件可删可重试。
  • 7. 多步表单:刷新页面草稿可恢复、回跳不丢数据、提交成功清草稿。
  • 8. 无障碍五项:label 关联、aria-describedby、aria-invalid、提交失败聚焦、必填文字标注。
  • 9. 移动端数字键盘:手机号、验证码、金额用 type="tel" 或 inputMode="numeric" 调起数字键盘;不要用 type="number"(它带出的是带加减 step 的键盘,且行为各浏览器不一致)。
  • 10. 自动填充:姓名、邮箱、电话、地址加上正确的 autocomplete 属性(name、email、tel……),移动端用户少打一半字。
  • 11. 密码管理器兼容:登录/注册表单用标准 type="password" + autocomplete="current-password|new-password",1Password/系统钥匙串能正常填充——自定义"显示密码"眼睛图标不要破坏这个。
  • 12. 错误上报:提交失败、异步校验异常打点上报,每周看一次 Top 错误字段——用户反复错的字段,就是文案或交互要改的地方。

表单优化没有终点,但有起点:先把第 4 节的状态机和第 1 节的三层分工落地,你的表单就已经超过 80% 的 vibe 项目了。剩下的,是每周看一次错误上报,让真实用户的报错替你排优化优先级。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

命令面板界面截图:搜索框、命令列表与右侧快捷键提示
指南
效率党的入口:vibe 项目的命令面板(Cmd+K)实战

AI 半小时就能交出一个 Cmd+K 面板,但匹配排序、快捷键冲突、输入法闪烁、移动端降级才是它翻车的地方。本文给一人团队一条完整路线:命令注册表数据模型、五步模糊匹配评分管线、带冲突检测的全局快捷键体系、无障碍键盘闭环、移动端入口降级、与权限联动的命令,以及上线前 10 项检查表。

工具技巧设计体验前端工程
文档站首页截图(Starlight 搭建):导航、搜索框与内容分区
指南
产品的第二官网:vibe 项目的文档站搭建实战

AI 能糊出像文档的页面,但信息架构、版本化、搜索、与代码同步才是文档站能用的关键。本文给一人团队完整路线:README/帮助中心/文档站决策表、亲核过的四家工具对照(Fumadocs/Starlight/Docusaurus/Mintlify)、四块内容加两条读者路径的信息架构、快照式版本化、DocSearch 与本地搜索、OpenAPI 驱动的 API 参考、有帮助吗反馈闭环,以及上线前 10 项检查表。

开发工作流工具技巧增长与营销