先判断错误是否属于当前页面
后台从文章列表快速切到分类页以后,新的页面数据已经取得,顶部却仍然显示加载失败。这种表现需要先核对错误来自哪一次请求。旧页面离开时主动取消的任务,可能经过通用请求错误钩子,并把网络错误写入共享状态;共享布局随后读取这个残留错误,就会把正常导航显示为失败。排查时记录页面切换、请求身份和错误写入时间,不要只根据当前页面是否有数据判断。本文讨论后台共享错误状态的归属,示例不代表已经执行过的真实网络测试结果。
网络记录中被取消的请求,与返回系统错误的请求应分开观察。前者可能源自路由离开、输入变化或替代任务,后者则需要展示恢复办法。控制台中的异常名称也只是线索,要结合取消信号与触发原因核对。若新页面本身请求失败,不能因为切页时存在取消就一律忽略。错误处理的目标是让界面只显示当前任务应负责的结果,而不是让错误提示数量尽可能少。保留任务归属,才能同时减少误报并保留真实故障。
共享错误状态需要明确生命周期
布局汇总多个后台接口的错误时,应记录稳定的请求身份,并说明错误何时创建、清除和失效。进入新路由以后,上一页不再相关的错误应该退出当前展示;同一任务开始重试时,可以清除该任务旧错误。不能只在成功响应时清理,因为一个离开的请求可能永远没有成功结果。也不能无条件删除所有错误,否则并发任务中仍然需要处理的权限或保存错误可能被另一个请求抹掉。清理范围应与任务或页面范围一致。
全局 pending 也存在相同要求。开始与结束钩子若分别从不同形式的请求对象计算键,可能留下永远无法删除的标记。相对地址、标准化地址和 Request 对象的字符串表示并不一定相同。可以在创建任务时确定身份,后续钩子沿用同一值。对于同一地址的并发请求,仅使用地址又可能不足,需要任务令牌或明确的并发约定。把这两组身份分清,可以避免一边修复错误提示,一边仍让加载状态持续残留。
用明确原因表达主动取消
主动取消可以由任务自己的 AbortController 发起,并记录原因,例如导航离开、输入已更新或组件销毁。捕获异常时,读取这一次任务的信号与原因,再决定是否进入用户可见错误。不要只对错误消息进行模糊的 abort 或 cancel 字符匹配,它可能把其他问题误判为正常取消。超时虽然也可能使用取消机制实现,但对用户来说通常意味着任务未按期完成,需要相应提示。因此,信号已中止本身也不一定足以决定完全忽略。
取消原因应由受控的任务管理代码产生,不能由服务端任意文本决定。外部接口返回了包含取消字样的错误消息,仍然需要按响应类型处理。请求工具还可能包装异常,把底层原因放在 cause 或其他字段中,核对时应使用当前版本支持的信息。示例中的显式原因只用于识别调用方自己的生命周期动作,不是一个通用错误分类器。真实系统还需要区分网络、权限、限流与服务器故障,保持各自的恢复路径。
逐个检查请求钩子的收尾路径
公共请求工具通常有开始、请求异常、成功响应和响应异常几个入口。先为每个入口画出共享状态的写入与删除操作,检查取消是否在请求异常阶段进入普通错误分支。处理响应异常时也要删除对应加载标记,但不能先把所有异常写进错误集合,再期待页面隐藏它。错误分类应在写入之前完成,布局只消费已经归属清楚的结果,避免每个显示位置自行猜测异常类型。
同一个稳定任务键要贯穿全部钩子。开始阶段从相对地址生成键,结束阶段却从标准化请求对象生成另一个键,会让清理找不到原登记。可以把键与页面范围保存在这次请求的闭包中,后续沿用,不重新从变化后的对象猜测。需要并发读取同一地址时,增加参数或任务标识区分,而不是用一个地址字段覆盖所有在途状态,导致某个请求结束就清除另一请求。
页面范围切换应先让旧汇总失效
后台布局可以保存当前路由范围编号,请求开始时取得该编号。导航进入新页面时更新范围,旧编号的错误不再属于可见集合。这样即使底层取消稍晚完成,捕获分支也知道自己已经失效。进入登录页时同样需要清除后台页面范围,避免此前管理接口的错误遮住公开登录表单。范围变化应发生在明确导航入口,不由每个页面重复猜测当前布局是否还存在。
布局汇总状态与真正停止底层工作仍是不同职责,取消控制器的完整设计可参考已公开的组合式函数文章。本篇关注汇总层如何拒绝旧范围回写。局部请求完成后,先检查任务令牌及范围,再更新错误;同范围的其他任务继续独立结算。真正的权限异常如果需要全局退出,应走专门会话策略,不能因它不属于当前页面就忽略账号安全边界。
错误恢复动作仍然要保留
真实失败应显示对当前任务有用的信息。未认证可以进入登录恢复,权限不足说明操作范围,系统暂时失败提供适用重试,限流则说明等待条件。不要为了修复切页误报,把请求错误钩子全部清空。重试时也要确认仍处于同一页面或同一资源条件,避免旧错误提示里的按钮重新请求已经离开的内容。错误与恢复动作需要共享相同的归属标识,这样点击重试才能更新正确任务。
对写入操作,取消浏览器请求不等于服务端已经撤销写入。保存文章时发生超时或导航,应根据接口契约查询结果,再决定是否重复提交。本文聚焦读取页面的误报,不能把忽略主动取消的规则直接推广到业务写入结果。错误提示可以说明状态尚待确认,而不是宣称保存失败或成功。保持这种适用边界,有助于防止一个看似友好的统一取消处理,掩盖实际上仍在进行的服务端动作。
用快速切页与延迟响应验证归属
可以在隔离环境让旧页面请求延迟,再切到新页面,确认旧任务取消以后没有进入当前可见错误。随后分别让旧任务晚到成功与晚到失败,检查新页面数据、错误和 pending 都没有被覆盖。还需要让新页面真的失败,证明错误仍然能显示,并在适用条件下重试恢复。测试应固定动作顺序和任务身份,不需要编造一个误报率数字;能够解释每一次回写属于哪个任务,就能形成有价值的验证记录。
并发场景也要覆盖,例如页面同时读取文章和分类,其中一项取消或失败,另一项仍在进行。共享汇总不能因为一项结束就认为全部完成,也不能因为一项成功就删除所有错误。对同地址不同参数的请求,需要确认身份方案能够区分版本。未实际执行的场景应保留待检查项,临时日志只记录任务事件与状态,不保存私人正文。验证完成以后,再移除调试输出,保留必要的错误关联记录即可。
避免把当前状态重置当作唯一修复
导航时重置错误可以清除已有残留,但如果旧请求随后还能写回,它只解决了一个短暂瞬间。因此,重置入口和回写条件需要同时检查。任务启动、成功、失败、取消和销毁都应遵循同一归属规则。对于全局状态,可以设置当前页面范围,再让旧范围的回调自然失效。不要依靠在布局中反复清空错误维持界面,那会让真实故障也被随时抹掉,并使请求工具无法说明最终状态。
错误分类还应集中到合适层级,避免每个页面各写一套模糊文本匹配。公共请求工具可以提供稳定原因,页面负责决定哪些信息属于当前展示。接口响应和客户端取消各有来源,处理时保留区分。对第三方库升级,核对包装异常与钩子顺序是否改变,再执行同样的切页测试。这样,误报处理就有一组可复核的契约,而不是只能靠某个当前版本的错误字符串碰巧匹配。
为共享布局提供已经归属清楚的结果
布局可以显示当前页面范围内的加载和错误,但不应该负责猜测一个异常是否来自旧任务。请求层与页面层先完成分类、归属和清理,再让布局读取稳定状态。这样,正常切页不会留下失败提示,真正失败仍能得到恢复动作,旧请求的结束也不会决定新页面的状态。评审时可以逐项查看所有异步回写,确认它们都检查了任务身份,并查看取消原因是否由调用方明确产生。
本文适用于后台读取请求和共享状态导致的切页误报。写入动作、导航鉴权和后台长任务需要各自的结果确认规则,不能用相同忽略策略覆盖。处理完成的标准是取消不会冒充当前失败,旧结果不影响新任务,并且真实错误可见且可恢复。只删除一条错误信息,或只捕获一个异常名称,都不能说明这些条件已经满足;最终还需要通过隔离场景中的实际状态检查收尾。
实现片段
type Entry = { scope: number; token: number; pending: boolean; error: string }
export function useAdminRequestLedger() {
const scope = useState('admin-page-scope', () => 0)
const sequence = useState('admin-request-token', () => 0)
const entries = useState<Record<string, Entry>>('admin-request-ledger', () => ({}))
function begin(taskKey: string) {
const owner = scope.value, token = ++sequence.value
entries.value[taskKey] = { scope: owner, token, pending: true, error: '' }
return (message = '', lifecycleCancelled = false) => {
const entry = entries.value[taskKey]
if (scope.value !== owner || entry?.token !== token) return
entry.pending = false
entry.error = lifecycleCancelled ? '' : message
}
}
function enterPage() { scope.value++; entries.value = {} }
// 请求钩子沿用 begin 返回的结算函数;取消分类在调用前完成。
// enterPage 由布局导航入口调用,不在每个请求开始时调用。
return { entries, begin, enterPage }
}
评论 · 0