前端防止重复 toast 相同报错

3 阅读1分钟
import axios, { AxiosError, AxiosHeaders, InternalAxiosRequestConfig } from 'axios';
import { toast } from 'react-hot-toast';
import { getAuthToken, removeAuthToken, BusinessCode } from './';

type ResponseBody<T = unknown> = { code: number; message: string } & (
  | { data: T; success: true }
  | { data: null; success: false }
);

// 防重复toast机制
const toastMessageCache = new Map<string, number>();
const TOAST_DEBOUNCE_TIME = 3000; // 3秒内不重复显示相同消息

const showToastWithDebounce = (message: string, type: 'success' | 'error' = 'error') => {
  const now = Date.now();
  const lastShownTime = toastMessageCache.get(message);

  // 如果消息在3秒内已经显示过,则跳过
  if (lastShownTime && now - lastShownTime < TOAST_DEBOUNCE_TIME) {
    return;
  }

  // 更新缓存并显示toast
  toastMessageCache.set(message, now);

  // 清理过期的缓存条目(可选,避免内存泄漏)
  for (const [cachedMessage, timestamp] of toastMessageCache.entries()) {
    if (now - timestamp > TOAST_DEBOUNCE_TIME) {
      toastMessageCache.delete(cachedMessage);
    }
  }

  if (type === 'error') {
    toast.error(message);
  } else {
    toast.success(message);
  }
};

export const getRequestClient = (config: { baseUrl: string }) => {
  const client = axios.create({
    baseURL: config.baseUrl,
    headers: { 'Content-Type': 'application/json' },
  });

  client.interceptors.request.use(authRequestInterceptor);
  client.interceptors.request.use(sentryRequestInterceptor);
  client.interceptors.response.use(
    (response) => {
      if (response?.data?.code === BusinessCode.UNAUTHORIZED) {
        showToastWithDebounce(response?.data?.message || '请登录', 'error');
        removeAuthToken();
        window.location.href = '/login';
        return response;
      }
      return response;
    },
    (error: AxiosError<ResponseBody>) => {
      showToastWithDebounce(error.response?.data?.message || '请求失败', 'error');
      return Promise.reject(error);
    },
  );
  return client;
};

const authRequestInterceptor = async (c: InternalAxiosRequestConfig) => {
  const token = getAuthToken();
  if (token) {
    c.headers = c.headers || new AxiosHeaders();
    c.headers.set(`Authentication`, `Bearer ${token.token}`);
  }
  return c;
};

const sentryRequestInterceptor = async (c: InternalAxiosRequestConfig) => {
  return c;
};

思路

使用一个 map 接受请求返回的 message,如果在 map 中不存在,就直接塞进 map 里,value 就是时间戳。如果存在,就拿出来对比一下现在的时间,是否在 3 秒以内,如果在 3 秒以内,说明已经提示过了,就直接丢弃这次 toast。最后清理一下 3 秒以外的过期缓存。