phase 5 part 1: typescript, vue, react, ssr(Next.js 16) topics with lessons, scenarios, interviews, verified examples

This commit is contained in:
Shikui Wang committed 2026-10-23 16:24:06 +08:00
1 parent 7a32d2c9a6
commit 7b5cae7d24
39 files changed
+2482 -48

No files matched your search

@@ -0,0 +1,71 @@
---
id: frontend.react.scenario.long-task-ui
title: 长任务界面:Agent 任务的状态机、事件去重、乐观更新和恢复
type: scenario
domain: frontend
stack: [react, typescript, fastapi]
tags: [任务状态, 状态机, useReducer, SSE, 幂等, 乐观更新, 异常恢复]
level: advanced
prerequisites: [frontend.react.hooks-rendering]
est_hours: 2.5
related: [frontend.react.interview, ai.langgraph.scenario.refund-approval, frontend.vue.scenario.ai-chat-stream]
week: [7, 12]
---
# 长任务界面:Agent 任务的状态机、事件去重、乐观更新和恢复
## 业务背景与问题
报表 Agent 生成一份报表要 1~3 分钟,中间可能需要用户补充信息(“要按大区还是按城市?”)。前端第一版的问题:
1. 用户刷新页面后,看不到正在运行的任务,以为丢了,又提交了一次;
2. 网络重连后收到重复的进度事件,步骤列表出现重复项;
3. 任务失败只显示“出错了”,用户不知道能不能重试;
4. 点击“取消”后界面立即显示已取消,但后端其实还在跑;
5. 同时打开两个标签页,状态不一致。
## 约束
- 后端是 LangGraph + FastAPI,任务状态存在数据库中,事件通过 SSE 推送。
## 方案设计
1. **任务以服务端为准**:提交后后端返回任务 ID;页面加载时先查询“我的进行中任务”并恢复显示;提交接口带幂等键(防止重复提交)。
2. **前端状态机**:`useReducer` 集中管理状态转换(排队 / 运行 / 等待输入 / 成功 / 失败 / 取消),事件带递增序号,旧事件和终态后的事件丢弃。
3. **重连**:断开后先调用查询接口拿到最新快照(包含当前序号),再从该序号之后订阅事件。
4. **失败分类**:后端返回 `retryable`,前端据此显示“重试”按钮或“联系管理员”。
5. **取消**:点击后进入“取消中”,后端确认(事件或接口返回)后再显示“已取消”;不做乐观的终态显示。
6. **等待输入**:显示问题和输入框,提交后通过接口恢复任务(对应后端的 `interrupt` / `Command(resume=...)`)。
7. **多标签页**:都以服务端事件为准;必要时用 `BroadcastChannel` 同步。
## 关键代码
状态机和组件见 [examples/task-panel](../examples/task-panel/)。重连时先取快照再订阅(示意):
```ts
async function connect(taskId: string, dispatch: (e: TaskEvent) => void, signal: AbortSignal) {
const snap = await fetchJSON<TaskSnapshot>(`/api/tasks/${taskId}`, { signal });
dispatch({ type: "snapshot", snapshot: snap }); // 用快照覆盖本地状态
await streamEvents(`/api/tasks/${taskId}/events?after=${snap.seq}`, dispatch, signal);
}
```
## 取舍
| 选择 | 优点 | 缺点 |
| --- | --- | --- |
| **服务端为准 + 前端状态机(本方案)** | 刷新、重连、多标签页都一致 | 需要快照接口和事件序号 |
| 只靠前端内存状态 | 简单 | 刷新即丢失 |
| 轮询任务状态 | 实现简单、天然可恢复 | 延迟和请求量;可作为 SSE 不可用时的降级 |
| 乐观更新所有操作 | 体验快 | 对“取消”这类不一定成功的操作会误导用户 |
## 易错点
- 状态转换分散在多个 `useState` 和事件处理函数中,难以保证一致。
- 不区分“已取消”和“取消中”。
- 重连后不取快照,直接订阅,错过断线期间的事件。
- `useEffect` 中订阅没有清理,切换任务后旧任务的事件串进来。
## 面试怎么讲
> “长任务界面我以服务端为准:提交带幂等键,页面加载时恢复进行中的任务。前端用 useReducer 做状态机,事件带序号,重复、乱序、终态后的事件都丢弃;断线重连先取快照再从序号之后订阅。失败按 retryable 显示重试,取消要等后端确认,不做乐观的终态。等待用户输入时,提交后调用接口恢复后端的 LangGraph 任务。reducer 是纯函数,状态转换都有单元测试。”