表单校验与提交流程
表单与 v-model · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
把表单从「能输入」做到「能提交」:实现字段级校验、错误提示的展示时机、提交中的禁用与重复提交防护,以及一个可测试的校验函数设计。这一节也是练习 C63 的完整思路。
问题场景
注册表单有邮箱、密码、确认密码三个字段。你的第一版实现:点提交时用 alert 报错,一次只报一个;用户改了输入错误提示不消失;网络慢时连点三次提交,注册了三个账号。校验逻辑还和模板搅在一起,想写测试无从下手。
核心解释
把校验拆成三层:规则层(纯函数,可测试)、状态层(错误对象、提交状态)、展示层(模板只负责渲染错误)。
规则层写成纯函数,输入值返回错误信息或空串:
// utils/validate.js
export function validateEmail(value) {
if (!value.trim()) return "请输入邮箱";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "邮箱格式不正确";
return "";
}
export function validatePassword(value) {
if (!value) return "请输入密码";
if (value.length < 8) return "密码至少 8 位";
if (!/[a-zA-Z]/.test(value) || !/\d/.test(value)) return "密码需同时包含字母和数字";
return "";
}
export function validateConfirm(password, confirm) {
if (!confirm) return "请再次输入密码";
if (password !== confirm) return "两次输入的密码不一致";
return "";
}
状态层用 reactive 持有错误信息,提交时统一校验:
<script setup>
import { computed, reactive, ref } from "vue";
import { validateConfirm, validateEmail, validatePassword } from "./utils/validate";
const form = reactive({ email: "", password: "", confirm: "" });
const errors = reactive({ email: "", password: "", confirm: "" });
const submitting = ref(false);
const submitted = ref(false);
const valid = computed(
() => !errors.email && !errors.password && !errors.confirm
);
function validateAll() {
errors.email = validateEmail(form.email);
errors.password = validatePassword(form.password);
errors.confirm = validateConfirm(form.password, form.confirm);
return valid.value;
}
async function submit() {
if (submitting.value) return; // 防重复提交
submitted.value = true; // 之后显示错误
if (!validateAll()) return;
submitting.value = true;
try {
await new Promise((r) => setTimeout(r, 600)); // 模拟请求
// 成功后跳转或清空
} finally {
submitting.value = false;
}
}
</script>
<template>
<form @submit.prevent="submit">
<label>
邮箱
<input v-model.trim="form.email" type="email" @blur="errors.email = validateEmail(form.email)" />
</label>
<p v-if="errors.email" class="error">{{ errors.email }}</p>
<label>
密码
<input v-model="form.password" type="password" @blur="errors.password = validatePassword(form.password)" />
</label>
<p v-if="errors.password" class="error">{{ errors.password }}</p>
<label>
确认密码
<input v-model="form.confirm" type="password" @blur="errors.confirm = validateConfirm(form.password, form.confirm)" />
</label>
<p v-if="errors.confirm" class="error">{{ errors.confirm }}</p>
<button type="submit" :disabled="submitting">
{{ submitting ? "提交中…" : "注册" }}
</button>
<p v-if="submitted && valid" class="ok">校验通过</p>
</form>
</template>
关键设计点:
- 错误提示的时机:
@blur时校验单个字段(用户离开才打扰),提交时校验全部。不要在每次输入时立刻报错——用户刚敲第一个字符就看到「邮箱格式不正确」是很糟的体验。 @submit.prevent:阻止表单默认提交(页面刷新)。Vue 的.prevent修饰符等价于event.preventDefault()。- 防重复提交:
submitting既控制按钮禁用,也在函数入口做守卫(if (submitting.value) return)——双保险,防止绕过按钮的调用。 - 校验函数与组件解耦:
validateEmail等是纯函数,可以单独 import 进测试文件跑断言,这是下一节「组件测试」的基础。
示例拆解
把「提交中禁用」和「错误随输入消失」两个体验细节补上:
<script setup>
import { reactive, ref, watch } from "vue";
import { validateEmail } from "./utils/validate";
const form = reactive({ email: "" });
const errors = reactive({ email: "" });
const submitting = ref(false);
const touched = ref(false);
// 用户已经看到过错误后,输入时实时清除(而不是实时新增)
watch(
() => form.email,
(value) => {
if (touched.value && errors.email) {
errors.email = validateEmail(value);
}
}
);
async function submit() {
if (submitting.value) return;
touched.value = true;
errors.email = validateEmail(form.email);
if (errors.email) return;
submitting.value = true;
try {
await fakeRequest();
} finally {
submitting.value = false;
}
}
</script>
这段的要点:touched 标记「用户是否已经触发过校验」。没触发过时输入不报错;触发过之后,输入立即重新校验——错误提示会随着用户改正而消失,体验自然。这是表单库(如 VeeValidate)内部的核心状态模型,自己手写一遍就理解了。
动手任务
- 完成上面的注册表单,验证三条链路:空提交报三个错、逐个修正错误消失、快速连点提交只发一次请求。
- 给密码加「强度提示」(弱/中/强),用 computed 派生,不写进 errors。
- 把校验函数复制出来,在 Node 里用
node --input-type=module跑几条断言(assert.equal(validateEmail("a@b.com"), "")),体会纯函数可测试的价值。
常见错误
- 每次 input 就校验:用户刚输入第一个字符就看到红字,体验差。用 blur 或 touched 标记。
- 错误信息只存一个字符串:多字段表单无法定位。用对象按字段存。
- 提交按钮只靠 disabled 防重复:键盘回车、脚本调用仍可触发。函数入口加守卫。
- 校验规则写死在模板里:
v-if="form.email && !form.email.includes('@')"无法复用、无法测试。抽成纯函数。
检查与复盘
- 能写出「规则层/状态层/展示层」三层结构的表单
- 能解释 touched 标记解决什么体验问题
- 能说出防重复提交的两道防线
参考资料
- Vue 官方文档「表单输入绑定」:修饰符与事件
- MDN:约束验证 API(浏览器原生校验的边界)
读完这一课了?
标记完成后,工作台会安排接下来的学习。