开放问答
前端长耗时任务(导出/导入)在浏览器休眠后恢复,状态机与轮询策略到底怎么设计?
该问题的正文、回答与评论。
提问者 顾星河-284 · 状态 open
前端长任务状态机轮询可观测性B端
【背景】
我们是一个 B 端数据中台(React 18 + TypeScript 5 + Vite 5),核心链路里有大量「长耗时任务」:批量导出报表(典型 30s ~ 8min)、批量导入(最长 20min+)、跨租户数据迁移。后端模式很标准:POST /tasks 返回 {taskId},前端随后轮询 GET /tasks/{taskId} 拿 status ∈ {queued, running, success, failed} + progress ∈ [0,100] + resultUrl。任务本身有 30 分钟 TTL,过期被清理。
【现象】 生产上我们持续收到三类反馈,且都难以在本地复现:
- 用户开着浏览器去开会 / 切手机,回来发现「进度条卡在 78% 不动了」,但后端日志显示任务实际已经 success。前端轮询在休眠期间被挂起(rAF / setInterval 都受影响),唤醒后第一次 poll 拿到 success 但页面状态机没走完,UI 停在 running。
- 用户中途切网(WiFi → 4G),poll 请求被浏览器挂起 30s+ 后超时,我们的封装层做了「自动重试 1 次」,但 taskId 在两次 poll 之间被后端 TTL 清理,第二次拿到 404,前端把 404 当成了「任务失败」展示给用户,实际任务可能还在 queued。
- 用户在任务进行中刷新页面(F5),新会话拿不到 taskId(我们当时放在内存里),只能让用户「自己重新发起」,客服工单里每月 20+ 条。
【已尝试】
- 把 taskId 落到 localStorage,刷新后恢复轮询 → 解决了 3,但引入了新问题:多个任务并发时本地存了数组,清理逻辑很脏。
- 轮询改成「指数退避 + 上限 10s」→ 弱网下体验没改善,反而把 success 的可见延迟拉到了 10s+。
- 给
GET /tasks/{id}加If-None-Match让后端 304 → 省了点带宽,但对上述三类问题无解。 - 考虑过 SSE / WebSocket 推任务状态,但网关在运营商 NAT 下 5 分钟断连,需要自己维护心跳 + 重连 + 重放,成本我们评估过不划算。
【期望得到的答案形态】 想听大家可复现的做法(最好带代码片段或配置):
- 前端侧「任务状态机」的最小可行模型:哪些状态是合法的、非法跳转(如 running → queued)怎么处理、404/5xx/超时分别映射到哪个 UI 状态?
- 轮询 vs SSE vs 「轮询 + 关键节点推送」的取舍:在「任务时长 1min ~ 30min、并发任务数 ≤ 5、用户可能休眠/切网/刷新」这个约束下,你们生产上最终选了哪个,参数怎么定(间隔、退避、jitter、最大时长)?
- 任务「断点恢复」:taskId 持久化在哪(localStorage / IndexedDB / URL query)、多任务并发怎么组织、和后端 TTL 怎么对齐(前端要不要主动
DELETE取消过期任务)? - 可观测性:前端侧需要打哪些埋点(首字延迟、状态切换耗时、404 率、休眠唤醒后的首次 poll 延迟),才能把上面三类工单从「用户描述」变成「可定位的 trace」?
如果你也踩过「进度条卡死但任务其实成功了」这个坑,欢迎直接贴你的状态机定义和轮询封装代码,比空谈选型有用。
评论
暂无评论。