zhengyiming
2025-03-06 32b9ce7361f9410a3540ec4f2db3ae6c4d2d2728
apps/h5/src/utils/request/index.ts
New file
@@ -0,0 +1,303 @@
import { type IRequestOptions, BoleRequest, type RequestConfig } from '@bole-core/core';
import { type AxiosRequestConfig, type AxiosError } from 'axios';
import qs from 'qs';
import NProgress from '../progress';
import { loadEnv } from '@build/index';
import { getToken, getUserInfo } from '../storage';
import { useUserStoreHook } from '@/store/modules/user';
import { ElLoading as Loading, ElNotification } from 'element-plus';
import { router } from '@/router';
import { Message, tokenIsExpired } from '@/utils';
// 加载环境变量 VITE_PROXY_DOMAIN(开发环境)  VITE_PROXY_DOMAIN_REAL(打包后的线上环境)
const { VITE_PROXY_DOMAIN, VITE_PROXY_DOMAIN_REAL, DEV } = loadEnv();
const RefreshTokenUrl = '/GetTokenByRefreshToken';
/** 请求白名单,放置一些不需要token的接口(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */
const whiteList = [RefreshTokenUrl, '/GetToken'];
let loadingInstance: ReturnType<typeof Loading.service>;
function startLoading() {
  loadingInstance = Loading.service({
    fullscreen: true,
    lock: true,
    background: 'transparent',
  });
}
function endLoading() {
  if (loadingInstance) {
    loadingInstance.close();
  }
}
// 错误处理方案: 错误类型
enum ErrorShowType {
  /**
   * 啥也不干
   */
  SILENT = 0,
  WARN_MESSAGE = 1,
  ERROR_MESSAGE = 2,
  /**
   * 通知报错
   */
  NOTIFICATION = 3,
  /**
   * 重定向
   */
  REDIRECT = 9,
}
// 与后端约定的响应数据格式
interface ResponseStructure {
  success: boolean;
  data: any;
  /**
   * 错误码
   */
  error?: number;
  /**
   * 错误信息
   */
  msg?: string;
  showType?: ErrorShowType;
}
interface ErrorResponse {
  error?: {
    data: any;
    code: number;
    message: string;
  };
}
interface ErrorInfo {
  errorCode?: number;
  errorMessage?: string;
  showType?: ErrorShowType;
  data: any;
}
const AxiosOptions: AxiosRequestConfig = {
  baseURL: DEV ? VITE_PROXY_DOMAIN : VITE_PROXY_DOMAIN_REAL,
  timeout: 30000,
  headers: {
    Accept: 'application/json, text/plain, */*',
    'Content-Type': 'application/json',
    'X-Requested-With': 'XMLHttpRequest',
  },
  // 数组格式参数序列化
  paramsSerializer: {
    //@ts-ignore
    serialize: (params) => qs.stringify(params, { indices: false }),
  },
  withCredentials: true,
};
const config: RequestConfig<ResponseStructure, IRequestOptions> = {
  ...AxiosOptions,
  errorConfig: {
    // 错误接收及处理
    errorHandler: (error, opts) => {
      console.log('error: ', error);
      if (opts?.skipErrorHandler) throw error;
      if (opts?.customErrorHandler) {
        if (opts?.customErrorHandler(error)) {
          return;
        }
      }
      // 我们的 errorThrower 抛出的错误。
      if (error.name === 'BizError') {
        const errorInfo: ErrorInfo | undefined = (error as any).info;
        if (errorInfo) {
          const { errorMessage, errorCode } = errorInfo;
          switch (errorInfo.showType) {
            case ErrorShowType.SILENT:
              // do nothing
              break;
            case ErrorShowType.WARN_MESSAGE:
              Message.warnMessage(errorMessage);
              break;
            case ErrorShowType.ERROR_MESSAGE:
              Message.errorMessage(errorMessage);
              break;
            case ErrorShowType.NOTIFICATION:
              ElNotification.error({
                message: `${errorCode}: ${errorMessage}`,
              });
              break;
            case ErrorShowType.REDIRECT:
              // TODO: redirect
              break;
            default:
              Message.errorMessage(errorMessage);
          }
        }
      } else if ((error as AxiosError<ResponseStructure, IRequestOptions>).response) {
        // Axios 的错误
        // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
        handleAxiosResponseError(error as AxiosError<ErrorResponse, IRequestOptions>);
        // Message.errorMessage(`Response status:${(error as AxiosError).response.status}`);
      } else if ((error as AxiosError).request) {
        // 请求已经成功发起,但没有收到响应
        // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
        // 而在node.js中是 http.ClientRequest 的实例
        Message.errorMessage('服务器无响应,请重试');
      } else {
        // 发送请求时出了点问题
        Message.errorMessage('发送请求时出了点问题');
      }
    },
    // 错误抛出
    errorThrower: (res) => {
      const { success, data, error: errorCode, msg, showType } = res;
      if (!success) {
        const error: any = new Error(msg);
        error.name = 'BizError';
        error.info = { errorCode, errorMessage: msg, showType, data };
        throw error; // 抛出自制的错误
      }
    },
  },
  requestInterceptors: [
    [
      (config) => {
        const $config = config;
        // 开启进度条动画
        if (config.needNProcess) {
          NProgress.start();
        }
        const token = getToken();
        const userInfo = getUserInfo();
        const { showLoading = true, mock } = $config;
        if (mock) {
          $config.url = $config.url.replace(/^\/api/, '');
        }
        /**
         * 如果是refreshToken这个方法 就直接返回 防止内存泄漏
         */
        if (whiteList.some((url) => $config.url.toLowerCase().includes(url.toLowerCase()))) {
          return $config;
        }
        if (showLoading) {
          startLoading();
        }
        return new Promise((resolve) => {
          if (token && $config.withCredentials) {
            if (tokenIsExpired(userInfo)) {
              if (!BoleRequest.refreshTokenPending) {
                const userStore = useUserStoreHook();
                BoleRequest.refreshTokenPending = true;
                // token过期刷新
                userStore
                  .refreshToken({
                    refreshToken: userInfo.refreshToken,
                    clientId: userStore.accountInfo.client_id,
                  })
                  .then((res) => {
                    $config.headers['Authorization'] = 'Bearer ' + res.accessToken;
                    BoleRequest.requests.forEach((cb) => cb(res.accessToken));
                    BoleRequest.requests = [];
                  })
                  .finally(() => {
                    BoleRequest.refreshTokenPending = false;
                  });
              }
              resolve(BoleRequest.retryOriginalRequest($config));
            } else {
              $config.headers['Authorization'] = 'Bearer ' + token;
              resolve($config);
            }
          } else {
            resolve($config);
          }
        });
      },
      (error: AxiosError) => {
        endLoading();
        return Promise.reject(error);
      },
    ],
  ],
  responseInterceptors: [
    [
      (response) => {
        const $config = response.config as IRequestOptions;
        const { needNProcess, getResponse = false } = $config;
        // 关闭进度条动画
        if (needNProcess) {
          NProgress.done();
        }
        endLoading();
        return getResponse ? response : (response.data as any).result;
      },
      (error) => {
        endLoading();
        return Promise.reject(error);
      },
    ],
  ],
};
const ErrorMessageMap = {
  [400]: '请求错误',
  [401]: '未授权,请登录',
  [403]: '拒绝访问',
  [404]: '请求地址出错',
  [408]: '请求超时',
  [500]: '服务器内部错误',
  [501]: '服务未实现',
  [502]: '网关错误',
  [503]: '服务不可用',
  [504]: '网关超时',
  [505]: 'HTTP版本不受支持',
};
function handleAxiosResponseError(error: AxiosError<ErrorResponse, IRequestOptions>) {
  if (error.response.config.url.toLowerCase().includes(RefreshTokenUrl.toLowerCase())) {
    handleLogout();
    return;
  }
  if (error && error.response) {
    let message = ErrorMessageMap[error.response?.status] ?? '请求错误';
    if (error.response.data?.error?.message) {
      message = error.response.data?.error?.message;
    }
    if (error.response?.status === 401) {
      handleLogout();
    }
    Message.errorMessage(message);
  }
}
function handleLogout() {
  const userStore = useUserStoreHook();
  let path = '/';
  if (userStore.token) {
    path = router.currentRoute.value.fullPath;
  }
  userStore.logout();
  router.replace(`/login?redirect=${path}`);
}
export const request = BoleRequest.create(config);