必须用显式状态机+AbortController+幂等键+本地暂存四层协同:idle→submitting→pending→done,超时需AbortController主动中断fetch防静默hang,6–8s为宜。
弱网环境下表单提交失败,不是“要不要重试”的问题,而是“根本不知道请求发没发出、到哪一步了、能不能安全重试”的状态失控。必须用显式状态机 + AbortController + 幂等键 + 本地暂存四层协同,缺一不可。
这是弱网最典型的静默失败:TCP 连接被系统中断,fetch 既不 resolve 也不 reject,UI 卡住。不能只靠 .catch() 捕获错误。
AbortController 主动设超时,例如 const controller = new AbortController(); setTimeout(() => controller.abort(), 8000)
fetch(..., { signal: controller.signal }) 会抛出 AbortError,可被捕获并进入失败分支then 外层——它不解决静默 hang,只解决慢响应状态机不是画流程图自嗨,它要映射到真实 DOM 和存储行为。关键状态只有四个:idle → submitting → pending → done,且每个状态必须有明确的副作用。
idle:按钮可点,localStorage 无对应 pending key,表单未被序列化submitting:按钮 disabled = true,立即调用 event.preventDefault(),同步生成幂等 key(如 crypto.randomUUID()),把 FormData 转为对象并存入 localStorage.setItem(`pending-form-${key}`, JSON.stringify({...}))
pending:请求已发但未返回,此时若页面刷新或切后台,靠 localStorage 恢复状态;监听 window.addEventListener('online') 只作信号,不自动重发done:成功则清除 localStorage 对应项;失败且满足重试条件(navigator.onLine && document.hidden === false && retryCount )才用 <code>setTimeout 延迟 2s 后重发,且重试后 retryCount++
单纯用 Date.now() 或 Math.random() 生成 key,会导致同一份表单多次提交被服务端视为不同请求,重复扣款、重复注册就来了。
立即学习“前端免费学习笔记(深入)”;
sha256(JSON.stringify(sortedEntries)),再拼上时间戳防碰撞crypto.randomUUID() + 表单 action URL + 所有非空字段名排序后的字符串,三者拼接后取前 16 字符input.files[0].name 等易变字段参与哈希——文件名可被用户随意修改,破坏幂等性不能。SW 对 POST 请求的缓存和重试支持极差,且多数采集接口明确禁用 SW 缓存。
workbox-strategies 的 NetworkOnly 或 StaleWhileRevalidate 都不适用于表单提交——前者无重试,后者会缓存失败响应fetch 事件中无法访问页面 DOM 或 FormData,拿不到原始表单数据,更没法注入幂等头最容易被忽略的是“重试时机”——它不取决于网络恢复,而取决于用户是否还在操作上下文里。document.hidden、visibilitychange、用户点击按钮这三者才是重试触发的真实锚点,而不是 online 事件本身。