登录注册表单校验与异步规则处理
概述
登录注册表单从"可提交"推进到"受控可验证",关键在于 Element Plus 的校验三要素协同:form 提供真实数据、rules 描述每个字段的规则、prop 把 FormItem 与字段名对应起来。本章在已抽离的 LoginForm 上补齐必填、长度、格式与交叉字段校验,强调提交前显式调用 validate(),并重点处理密码与确认密码互相触发校验时的死循环风险。
学习目标
- 掌握
form、rules、prop三者一致才能生效的校验基础 - 先加必填、长度、格式三类基础规则
- 让
FormRules围绕表单模型LoginFormState而非组件 props 建模 - 用自定义
validator实现确认密码这类交叉字段校验 - 覆盖
blur与change触发时机,并在校验前trim()防空格 - 提交前显式
formRef.validate(),并用防抖打断双字段互校验死循环
一、校验三要素:form、rules、prop 协同
Element Plus 的校验不是每个输入框各自乱写判断,而是三部分协同:form 是真实表单数据、rules 是每个字段对应规则、prop 把某个 FormItem 和 rules / form 中的字段名对应起来。只要这三者没对齐,校验就不会按预期生效——只写了 rules 却没写 prop,当前项不会触发校验。
<el-form :model="form" :rules="rules">
<el-form-item prop="username">
<el-input v-model="form.username" />
</el-form-item>
</el-form>二、先加必填、长度、格式三类基础规则
登录注册页最先加的规则通常是用户名必填、用户名最短最长、邮箱或用户名格式、密码必填、确认密码再次输入。登录页通常先处理"是否为空"与"输入是否基本合法",不要一上来就写过多业务规则;长度规则与格式规则常同时存在而非二选一;提示文案要具体,不要所有错误都只写"请输入用户名"。
username: [
{ required: true, message: "用户名不得为空", trigger: "blur" },
{ min: 6, max: 16, message: "用户名长度应为 6 到 16 位", trigger: "blur" }
]三、FormRules 应围绕表单模型而非 props
一个典型坑是把校验规则字段写在 LoginFormProps 之类的 props 类型上,导致 username、password 对不上。正确做法是单独定义 LoginFormState,再让 FormRules<LoginFormState> 针对这份表单模型工作——校验关注的是表单值,不是组件展示参数。props 解决组件入参,form state 解决表单值,二者不能混。
四、确认密码是交叉字段校验
rePassword 必须和 password 相同,这类规则依赖另一个字段,属于交叉字段校验,通常需要写成自定义 validator。校验本质依赖 form.password,提示文案应明确"两次输入不一致"而非笼统"格式错误"。
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 实例绑定,否则无法在提交前做整表校验。
async function onSubmit() {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
emit("submit", form)
}八、双字段互校验的死循环与防抖
在 password 校验里触发 rePassword 校验、又在 rePassword 校验里触发 password 校验,两边来回调用最终导致堆栈溢出,这是典型的交叉校验死循环。处理思路是给其中一侧或双方加延迟执行 / 防抖,避免同一时刻同步互相拉起校验。防抖不是为了好看,而是为了打断递归调用链。
const debouncedValidateRePassword = useDebounceFn(() => {
formRef.value?.validateField("rePassword")
}, 300)九、rules 底层是 async-validator
Element Plus 表单规则建立在 async-validator 之上,意味着除 required 和自定义 validator 外,还可复用很多标准规则:pattern、type、len、min、max。邮箱、手机号这类格式校验很多时候可直接复用现成规则或正则;自定义 validator 更适合交叉字段、异步校验、复杂条件判断。了解底层能力能少造很多轮子。
email: [{ type: "email", message: "邮箱格式不正确", trigger: "blur" }]
phone: [{ pattern: /^1\d{10}$/, message: "手机号格式不正确", trigger: "blur" }]常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 写了 rules 但页面没有任何校验效果 | 忘写 prop,或 prop 与字段名不一致 | 保证 form、rules、prop 三者字段完全对应 |
| 输入空格后表单也能通过 | 没对值做 trim() | 在自定义校验中先 trim() 再判断 |
| 有错误提示但点击登录仍提交了 | 只做字段级校验,没整表 validate() | 提交前显式调用 formRef.validate() |
| 确认密码规则总不稳定 | 只校验当前字段,没关联主密码 | 给 rePassword 增加交叉字段自定义校验 |
| 两密码框互校验死循环或栈溢出 | 双方同步立即触发对方校验 | 加防抖或延迟,控制触发频率 |
| 所有规则都写成自定义函数 | 没充分利用标准规则 | 简单场景优先用 required、min/max、pattern、type |