前端-弱网优化

2,786 阅读5分钟

根据产品经理的要求,考虑到用户在弱网环境下的使用体验,需要实现以下几点需求:

  1. 第一次加载时如果出错不显示toast
  2. 窗口聚焦自动请求时不显示toast
  3. 只有用户主动交互出错时显示toast

首先整理一下目前工作项目中网络请求的封装:

  1. 底层使用axios请求,组件层采用tanstack query的useQuery/useMutation
  2. axios的拦截器可以在请求或响应被then 或catch 处理前拦截它们,现有的方案就是在axios的拦截器里处理错误
  3. axios是基于promise的请求库,还可以在请求结束之后的then/catch方法中处理错误
  4. tanstack query最新版本中,可以在queryClient的throwOnError方法中捕获错误

基于全平台的组件都要优化,一开始想着直接基于useQuery再实现一个useHook,在封装好的hook方法中统一做处理,踩了不少坑

方式一:meta

基于queryClient的meta配置,但是

tanstack query的refetch方法不支持meta参数,无法传参,就无法区分默认请求和refech请求,

方式二:重写refetch

有两种方法

  1. 基于只要触发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,
  };
};

隐患:

  1. 频繁的queryKey变化可能会导致不必要的重新渲染和请求。
  2. QueryKey污染:通过修改时间戳参数强制触发请求,会导致:
    • 每次重试都会生成新的缓存键
    • 破坏React Query的智能缓存机制
    • 可能造成内存泄漏(累积无用的缓存条目)

担心这样做可能会破坏原有useQuery的queryKey(不确定),

  1. 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处理。

  • 优点:逻辑集中,通过请求配置决定是否抛出,组件层统一处理错误,更符合单一职责。
  • 缺点:需要确保所有相关请求正确传递配置参数,可能需要更细致的错误类型区分。