弱网环境下的表单提交,最让人头疼的往往不是“要不要重试”,而是“根本不知道请求发到哪了、有没有发出去、还能不能安全重试”。这种状态失控,才是真正的难题。解决办法也很明确:必须用显式状态机 + AbortController + 幂等键 + 本地暂存这四层协同,缺了哪个都不行。fetch 超时后进不了
弱网环境下的表单提交,最让人头疼的往往不是“要不要重试”,而是“根本不知道请求发到哪了、有没有发出去、还能不能安全重试”。这种状态失控,才是真正的难题。解决办法也很明确:必须用显式状态机 + AbortController + 幂等键 + 本地暂存这四层协同,缺了哪个都不行。

长期稳定更新的攒劲资源: >>>点此立即查看<<<
先说一个最常见的坑:TCP连接被系统中断后,fetch既不resolve也不reject,UI直接卡死。不能指望.catch()一个方法就搞定所有错误。
AbortController 主动设超时。比如 const controller = new AbortController(); setTimeout(() => controller.abort(), 8000),超时后fetch会抛出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 < 3),才用setTimeout延迟2s后重发,同时retryCount++。单纯用Date.now()或Math.random()生成key,同一份表单多次提交会被服务端当成不同请求,重复扣款、重复注册的麻烦也就来了。
sha256(JSON.stringify(sortedEntries)),再拼上时间戳防碰撞。crypto.randomUUID() + 表单action URL + 所有非空字段名排序后的字符串,三者拼接后取前16字符。input.files[0].name这类易变字段混进哈希——文件名用户可以随便改,幂等性就毁了。不能。Service Worker对POST请求的缓存和重试支持非常有限,而且很多采集接口明确禁用SW缓存。
workbox-strategies的NetworkOnly或StaleWhileRevalidate都不适用于表单提交——前者没有重试,后者会缓存失败响应。fetch事件里根本拿不到页面DOM或FormData,更没法注入幂等头。最后想提醒的是,重试时机最容易忽略——它不取决于网络恢复,而是取决于用户是否还在操作上下文里。document.hidden、visibilitychange、用户点击按钮,这三者才是重试触发的真实锚点,而不是online事件本身。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述