根据产品经理的要求,考虑到用户在弱网环境下的使用体验,需要实现以下几点需求:
- 第一次加载时如果出错不显示toast
- 窗口聚焦自动请求时不显示toast
- 只有用户主动交互出错时显示toast
首先整理一下目前工作项目中网络请求的封装:
- 底层使用axios请求,组件层采用tanstack query的useQuery/useMutation
- axios的拦截器可以在请求或响应被then 或catch 处理前拦截它们,现有的方案就是在axios的拦截器里处理错误
- axios是基于promise的请求库,还可以在请求结束之后的then/catch方法中处理错误
- tanstack query最新版本中,可以在queryClient的throwOnError方法中捕获错误
基于全平台的组件都要优化,一开始想着直接基于useQuery再实现一个useHook,在封装好的hook方法中统一做处理,踩了不少坑
方式一:meta
基于queryClient的meta配置,但是
tanstack query的refetch方法不支持meta参数,无法传参,就无法区分默认请求和refech请求,弃
方式二:重写refetch
有两种方法
- 基于只要触发queryKey中相关数据的setState,就可以重新请求,在refetch的时候,可以传一个参数,axios通过判断传入的参数来判断出错时是否toast
-
- 一开始传的slient===true/false,refetch之后,queryKey带了true,无法再更新成false或者不带slient变量,弃
- 改成传入时间变量,每次refetch的时候,都传入一个额外参数,time===new Date().getTime(),而默认请求只走默认参数,axios通过判断传入的参数是否有时间变量来判断是否toast,但是!!如果refech之后,窗口聚焦触发请求,也会带上时间变量,还需要重新监听窗口聚焦事件,实现如下:
补充:一开始并不是想着一层层往下传参数,而是拦截器和axios都不处理错误,在tanstack query的useQuery的throwOnError方法中统一处理错误,这个也踩坑了,①tanstack query的最新版本的useQuery中不支持onError/onSuccess方法,②也不支持throwOnError方法,③但是queryClient中有throwOnError方法,可以统一配置,④结果因为refetch的queryKey每次都是新的,统一配置的throwOnError无法捕获refetch的错误,无法toast,才改为通过请求方法携带参数一层层向下传
import {useState, useEffect} from 'react';
import {useQuery} from '@tanstack/react-query';
import {merge} from 'lodash';
export const useAutoRetryQuery = (options: any) => {
const {queryKey: baseQueryKey, defaultParams, queryFn} = options;
const [extraParam, setExtraParam] = useState<any>(null);
// 深度合并参数
const finalParams = extraParam ? merge({}, defaultParams, extraParam) : defaultParams;
// 稳定的QueryKey生成
const queryKey = [...baseQueryKey, finalParams ? finalParams : {}];
const query = useQuery({
queryKey,
queryFn: () => queryFn(finalParams),
refetchOnWindowFocus: false,
retry: false,
throwOnError: false,
});
// 重写新监听窗口可见事件
useEffect(() => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && !query.isFetching) {
const newExtraParam = new Date().getTime();
setExtraParam(newExtraParam);
}
};
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [query]);
// 重写refech
const handleRetry = () => {
const newExtraParam = new Date().getTime();
setExtraParam({time: newExtraParam});
};
return {
...query,
refetch: handleRetry,
};
};
隐患:
- 频繁的queryKey变化可能会导致不必要的重新渲染和请求。
- QueryKey污染:通过修改时间戳参数强制触发请求,会导致:
-
- 每次重试都会生成新的缓存键
- 破坏React Query的智能缓存机制
- 可能造成内存泄漏(累积无用的缓存条目)
担心这样做可能会破坏原有useQuery的queryKey(不确定), 弃
- queryClient自带的属性方法
-
- 用queryClient.refetchQueries重写refetch,但是方法不执行,弃
- 用queryClient.fetchQuery重写refetch,实现如下
补充:这个一开始尝试过,但放弃的原因是,queryKey都一样,如果想在queryClient.throwOnError方法中统一处理错误,无法区分是refetch请求还是其它请求,改完时间变量一层层向下传那个版本,才又重写了这个方法
import {useQuery, QueryClient} from '@tanstack/react-query';
import {handleError} from '@/api/base/fetcher';
export const useAutoRetryQuery = (options: any) => {
const {queryKey: baseQueryKey, queryFn} = options;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
throwOnError: false,
retry: false,
},
},
});
const query = useQuery({
queryKey: baseQueryKey,
queryFn: queryFn,
});
// 重写refetch方法
const handleRetry = async () => {
try {
await queryClient.fetchQuery({
queryKey: baseQueryKey,
queryFn: () => queryFn({throwOnError: true}), // 第二个参数控制是否抛错
retry: false,
});
} catch (error) {
handleError(error);
}
};
return {
...query,
refetch: handleRetry,
};
};
在没有查询到queryClient.fetchQeury方法,重写refetch受挫时,先用了下面的方式提测
方式三:请求方法的config中带一个isShowErrorNow变量
接口请求时,config中传入isShowErrorNow变量, 在axios中通过判断config中是否有isShowErrorNow这个变量,来判断错误是在axios中抛出,还是在错误组件的refetch中处理,组件中refech实现如下:
import {Box, Flex} from '@kuma-ui/core';
import {FC} from 'react';
import {useTranslation} from 'react-i18next';
import {handleError} from '@/api/base/fetcher';
import styles from './index.module.less';
interface ErrorComponentProps {
refetch: () => any;
}
export const ApiErrorComponent: FC<ErrorComponentProps> = ({refetch}) => {
const {t} = useTranslation();
//
const handleRefetch = async () => {
const {error} = await refetch();
handleError(error);
};
return (
<Flex justifyContent={'center'} alignItems={'center'} flexDir={'column'} width={'100%'} height={'100%'}>
<Box className={styles['error-tips']}>{t('网络连接失败,请稍候~')}</Box>
<Box onClick={handleRefetch} className={styles['error-btn']}>
{t('重试')}
</Box>
</Flex>
);
};
总结两种方式的优劣:
方式二:包装useQuery,修改refetch传递参数。
- 优点:可能直接在包装层处理参数传递,统一管理。
- 缺点:需要维护参数传递链,可能增加组件和请求之间的耦合,不够灵活。
方式三:利用axios配置和useQuery的onError处理。
- 优点:逻辑集中,通过请求配置决定是否抛出,组件层统一处理错误,更符合单一职责。
- 缺点:需要确保所有相关请求正确传递配置参数,可能需要更细致的错误类型区分。