因为未调用event.preventdefault()阻止默认提交,fetch异步执行无法阻断浏览器同步跳转;必须在submit事件首行同步调用preventdefault(),再手动控制校验、状态更新与最终提交。

为什么 submit 事件里调用 fetch 后表单还是提交了?
因为 fetch 是异步的,submit 事件处理器里没阻止默认行为,浏览器照常提交表单——哪怕你写了 fetch 请求校验接口,也根本等不到响应返回。
必须显式调用 event.preventDefault(),再手动控制后续流程:
- 先禁用提交按钮(防重复点击)
- 发起
fetch校验请求 - 响应成功且服务端返回
{ valid: true },再调用form.submit() - 失败则显示错误、恢复按钮状态
fetch 校验失败时,如何把后端错误塞进对应 input 的 setCustomValidity?
服务端校验不通过时,通常会返回字段级错误,比如 { email: "邮箱已被注册" }。这时别只弹个 alert,要让原生表单验证 API 能接管:
- 遍历响应对象的每个键,找到对应
name属性的input元素 - 对每个出错字段调用
input.setCustomValidity(errorMsg) - 紧接着调用
input.reportValidity()触发红边框和气泡提示 - 注意:调用
setCustomValidity('')才能清空错误,设为''(空字符串)而非null或undefined
示例片段:
const errors = await response.json(); // { password: "至少8位" }
Object.entries(errors).forEach(([name, msg]) => {
const el = form.querySelector(`[name="${name}"]`);
if (el) {
el.setCustomValidity(msg);
el.reportValidity();
}
});
用户连输带点“提交”时,怎么避免并发请求打架?
连续快速点击提交,可能触发多个 fetch 请求,后发的响应可能比先发的晚到,导致错误状态被覆盖(比如第一次返回失败,第二次返回成功,但第二次响应先更新了 UI)。
最轻量的解法是加一个请求锁:
- 声明一个
let isSubmitting = false变量(闭包或组件级) - 在
submit处理器开头判断:若isSubmitting为true,直接return - 设
isSubmitting = true,请求结束后(无论成功失败)再设回false - 顺手禁用按钮:
button.disabled = true,完成后恢复
不用上 AbortController ——除非你真需要中止正在发送的请求(比如用户切走页面),普通表单校验场景,锁住入口更简单可靠。
校验接口返回 4xx/5xx 时,fetch 为什么不进 catch?
fetch 只在网络异常(如断网、DNS失败)时 reject,HTTP 状态码 400、500 等都算“成功响应”,会进 then 分支。很多人因此漏处理业务错误。
正确做法是在 then 里检查 response.ok 或 response.status:
if (!response.ok) throw new Error(`${response.status} ${response.statusText}`)- 或者直接
if (response.status >= 400) return response.json().then(data => Promise.reject(data)) - 这样就能统一在
catch里处理所有校验失败(含网络失败 + HTTP 错误)
否则,400 响应进了 then,你却只在 catch 里写错误提示,结果什么也不显示。
异步校验真正的难点不在发请求,而在把服务端反馈精准映射到前端表单状态,并守住并发和生命周期边界。多数 bug 出在没拦住默认提交、没清空旧的 setCustomValidity、或把 HTTP 错误当网络错误处理。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











