竞态请求 已经是一个老生常谈的问题,举个例子, 有个日志表格,用户可以点击详情来获取这条日志的详细信息,用户首先点击了 1806 这条数据,请求发送后还没从服务器响应的时候,又点击了 1804 这条数据,这时候 第一次请求 的数据返了回来,用户便看到了 1806 的数据,这便发生了所谓的 请求竞争 现象,于是,用户给你报了一个严重级别的 BUG
很明显,这个问题大概率出现 网络不稳定的情况下,当然也可能是来自 sql 语句写的问题
如何解决
有两种:
- 说服 甲方 or 产品 ,通过增加 loading 状态来解决,参考 ant-proTable 的解决办法
- 终止(取消)上次请求,只响应获取最新的请求返回值 方案二是一种比较通用且没那么费劲的解决办法
在 umijs 中该怎么解决
如果你使用的是 umijs@4 的请求,那么本文的方法还有用,毕竟底层还是依赖 axios 实现的,参考 axios 官网,已经支持 AbortController 取消请求。
因为 只保留最新的请求,那么只要把历史的 controller 取消了即可,可以用 闭包 解决,但既然是 React, useRef 保存的对象,不就是天然的闭包对象吗,将原来的请求再 包裹 下,核心逻辑代码如下:
const umiCancel = useRef<AbortController | null>(null);
const newRequest = () => {
if (umiCancel.current) {
umiCancel.current.abort();
}
const controller = new AbortController();
umiCancel.current = controller;
return request('/api/xx', { signal: controller.signal })
}
但,如果你的项目启用了类似 plugin-openapi 这种生成 service 能力的功能, 推荐你使用 @siroi/use-once-request这个插件
使用方法
pnpm add @siroi/use-once-request
它有两个 hook
- useRaceRequest :适用于修改单一请求,即只包装一个请求
- useRaceRequests : 适用于批量修改请求,传入一个请求数组,对每一个请求都进行包装返回 想法逻辑:因为自动生成的 service 代码一定包含 axios 的配置项,那么便可以通过 service 代码的参数和传入的参数个数来更新 axios 的配置项能力。
eg. 生成的接口
export async function createDept(body: API.DeptCreateReqVO, options?: { [key: string]: any }) {
return request<API.CommonResultLong>('/api/v1/system/demo/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),
});
}
业务使用
import useRaceRequest, {useRaceRequests} from "@siroi/use-once-request"
const currentRequest = useRaceRequest(createDept);
//or
const [currentRequest] = useRaceRequesrs([createDept]);
让我们启个 express 模拟一下慢接口:
最终效果