{T}

登录注册表单校验与异步规则处理

概述

登录注册表单从"可提交"推进到"受控可验证",关键在于 Element Plus 的校验三要素协同:form 提供真实数据、rules 描述每个字段的规则、propFormItem 与字段名对应起来。本章在已抽离的 LoginForm 上补齐必填、长度、格式与交叉字段校验,强调提交前显式调用 validate(),并重点处理密码与确认密码互相触发校验时的死循环风险。

学习目标

  • 掌握 formrulesprop 三者一致才能生效的校验基础
  • 先加必填、长度、格式三类基础规则
  • FormRules 围绕表单模型 LoginFormState 而非组件 props 建模
  • 用自定义 validator 实现确认密码这类交叉字段校验
  • 覆盖 blurchange 触发时机,并在校验前 trim() 防空格
  • 提交前显式 formRef.validate(),并用防抖打断双字段互校验死循环

一、校验三要素:form、rules、prop 协同

Element Plus 的校验不是每个输入框各自乱写判断,而是三部分协同:form 是真实表单数据、rules 是每个字段对应规则、prop 把某个 FormItemrules / form 中的字段名对应起来。只要这三者没对齐,校验就不会按预期生效——只写了 rules 却没写 prop,当前项不会触发校验。

Vue SFC
<el-form :model="form" :rules="rules">
  <el-form-item prop="username">
    <el-input v-model="form.username" />
  </el-form-item>
</el-form>

二、先加必填、长度、格式三类基础规则

登录注册页最先加的规则通常是用户名必填、用户名最短最长、邮箱或用户名格式、密码必填、确认密码再次输入。登录页通常先处理"是否为空"与"输入是否基本合法",不要一上来就写过多业务规则;长度规则与格式规则常同时存在而非二选一;提示文案要具体,不要所有错误都只写"请输入用户名"。

ts
username: [
  { required: true, message: "用户名不得为空", trigger: "blur" },
  { min: 6, max: 16, message: "用户名长度应为 6 到 16 位", trigger: "blur" }
]

三、FormRules 应围绕表单模型而非 props

一个典型坑是把校验规则字段写在 LoginFormProps 之类的 props 类型上,导致 usernamepassword 对不上。正确做法是单独定义 LoginFormState,再让 FormRules<LoginFormState> 针对这份表单模型工作——校验关注的是表单值,不是组件展示参数。props 解决组件入参,form state 解决表单值,二者不能混。

四、确认密码是交叉字段校验

rePassword 必须和 password 相同,这类规则依赖另一个字段,属于交叉字段校验,通常需要写成自定义 validator。校验本质依赖 form.password,提示文案应明确"两次输入不一致"而非笼统"格式错误"。

ts
const validateRePassword = (_rule: unknown, value: string, callback) => {
  if (!value.trim()) {
    callback(new Error("请再次输入密码"))
    return
  }
  if (value.trim() !== form.password.trim()) {
    callback(new Error("两次输入的密码不一致"))
    return
  }
  callback()
}

五、trigger 至少覆盖 blur 与 change

只在一个时机校验,体验会不完整:只在 change 校验,用户移出输入框时不会立刻得到反馈;只在 blur 校验,持续输入时反馈又太慢。登录注册这类高频输入表单,通常让 blur(适合必填校验)与 change(适合动态一致性校验)配合。若校验逻辑很重,change 频繁触发时就要考虑防抖。

六、校验前先 trim() 防空格

用户输入空格时看起来像填了内容,实际对登录毫无意义。在自定义校验里先 value.trim() 再判断,能避免"只输空格也算通过"的低级问题。这类细节虽小,但很能体现表单质量,更适合统一放在自定义校验中处理。

七、提交前必须显式 validate()

页面已有错误提示但点击登录时数据仍被直接提交,说明只加字段级规则还不够,提交动作前必须主动触发一次整表校验。字段级校验负责过程反馈,整表 validate() 负责提交兜底;formRef 必须和 el-form 实例绑定,否则无法在提交前做整表校验。

ts
async function onSubmit() {
  if (!formRef.value) return
  const valid = await formRef.value.validate()
  if (!valid) return
  emit("submit", form)
}

八、双字段互校验的死循环与防抖

password 校验里触发 rePassword 校验、又在 rePassword 校验里触发 password 校验,两边来回调用最终导致堆栈溢出,这是典型的交叉校验死循环。处理思路是给其中一侧或双方加延迟执行 / 防抖,避免同一时刻同步互相拉起校验。防抖不是为了好看,而是为了打断递归调用链。

ts
const debouncedValidateRePassword = useDebounceFn(() => {
  formRef.value?.validateField("rePassword")
}, 300)

九、rules 底层是 async-validator

Element Plus 表单规则建立在 async-validator 之上,意味着除 required 和自定义 validator 外,还可复用很多标准规则:patterntypelenminmax。邮箱、手机号这类格式校验很多时候可直接复用现成规则或正则;自定义 validator 更适合交叉字段、异步校验、复杂条件判断。了解底层能力能少造很多轮子。

ts
email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }]
phone: [{ pattern: /^1\d{10}$/, message: "手机号格式不正确", trigger: "blur" }]

常见问题

问题原因解决方案
写了 rules 但页面没有任何校验效果忘写 prop,或 prop 与字段名不一致保证 formrulesprop 三者字段完全对应
输入空格后表单也能通过没对值做 trim()在自定义校验中先 trim() 再判断
有错误提示但点击登录仍提交了只做字段级校验,没整表 validate()提交前显式调用 formRef.validate()
确认密码规则总不稳定只校验当前字段,没关联主密码rePassword 增加交叉字段自定义校验
两密码框互校验死循环或栈溢出双方同步立即触发对方校验加防抖或延迟,控制触发频率
所有规则都写成自定义函数没充分利用标准规则简单场景优先用 requiredmin/maxpatterntype

延伸阅读