浏览代码

feat: add request retry (#1553)

Captain 3 年之前
父节点
当前提交
136cbb1e3b

+ 8 - 0
mock/sys/user.ts

@@ -111,4 +111,12 @@ export default [
       return resultSuccess(undefined, { message: 'Token has been destroyed' });
     },
   },
+  {
+    url: '/basic-api/testRetry',
+    statusCode: 405,
+    method: 'get',
+    response: () => {
+      return resultError('Error!');
+    },
+  },
 ] as MockMethod[];

+ 14 - 0
src/api/sys/user.ts

@@ -8,6 +8,7 @@ enum Api {
   Logout = '/logout',
   GetUserInfo = '/getUserInfo',
   GetPermCode = '/getPermCode',
+  TestRetry = '/testRetry',
 }
 
 /**
@@ -39,3 +40,16 @@ export function getPermCode() {
 export function doLogout() {
   return defHttp.get({ url: Api.Logout });
 }
+
+export function testRetry() {
+  return defHttp.get(
+    { url: Api.TestRetry },
+    {
+      retryRequest: {
+        isOpenRetry: true,
+        count: 5,
+        waitTime: 1000,
+      },
+    },
+  );
+}

+ 1 - 0
src/locales/lang/en/routes/demo.ts

@@ -92,6 +92,7 @@ export default {
     breadcrumb: 'Breadcrumbs',
     breadcrumbFlat: 'Flat Mode',
     breadcrumbFlatDetail: 'Flat mode details',
+    requestDemo: 'Retry request demo',
 
     breadcrumbChildren: 'Level mode',
     breadcrumbChildrenDetail: 'Level mode detail',

+ 1 - 0
src/locales/lang/zh-CN/routes/demo.ts

@@ -88,6 +88,7 @@ export default {
     ws: 'websocket测试',
     breadcrumb: '面包屑导航',
     breadcrumbFlat: '平级模式',
+    requestDemo: '测试请求重试',
     breadcrumbFlatDetail: '平级详情',
     breadcrumbChildren: '层级模式',
     breadcrumbChildrenDetail: '层级详情',

+ 9 - 0
src/router/routes/modules/demo/feat.ts

@@ -31,6 +31,15 @@ const feat: AppRouteModule = {
         title: t('routes.demo.feat.ws'),
       },
     },
+    {
+      path: 'request',
+      name: 'RequestDemo',
+      // @ts-ignore
+      component: () => import('/@/views/demo/feat/request-demo/index.vue'),
+      meta: {
+        title: t('routes.demo.feat.requestDemo'),
+      },
+    },
     {
       path: 'session-timeout',
       name: 'SessionTimeout',

+ 4 - 1
src/utils/http/axios/Axios.ts

@@ -111,7 +111,10 @@ export class VAxios {
     // Response result interceptor error capture
     responseInterceptorsCatch &&
       isFunction(responseInterceptorsCatch) &&
-      this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
+      this.axiosInstance.interceptors.response.use(undefined, (error) => {
+        // @ts-ignore
+        responseInterceptorsCatch(this.axiosInstance, error);
+      });
   }
 
   /**

+ 28 - 0
src/utils/http/axios/axiosRetry.ts

@@ -0,0 +1,28 @@
+import { AxiosError, AxiosInstance } from 'axios';
+/**
+ *  请求重试机制
+ */
+
+export class AxiosRetry {
+  /**
+   * 重试
+   */
+  retry(AxiosInstance: AxiosInstance, error: AxiosError) {
+    // @ts-ignore
+    const { config } = error.response;
+    const { waitTime, count } = config?.requestOptions?.retryRequest;
+    config.__retryCount = config.__retryCount || 0;
+    if (config.__retryCount >= count) {
+      return Promise.reject(error);
+    }
+    config.__retryCount += 1;
+    return this.delay(waitTime).then(() => AxiosInstance(config));
+  }
+
+  /**
+   * 延迟
+   */
+  private delay(waitTime: number) {
+    return new Promise((resolve) => setTimeout(resolve, waitTime));
+  }
+}

+ 1 - 1
src/utils/http/axios/axiosTransform.ts

@@ -48,5 +48,5 @@ export abstract class AxiosTransform {
   /**
    * @description: 请求之后的拦截器错误处理
    */
-  responseInterceptorsCatch?: (error: Error) => void;
+  responseInterceptorsCatch?: (axiosInstance: AxiosResponse, error: Error) => void;
 }

+ 15 - 1
src/utils/http/axios/index.ts

@@ -17,6 +17,7 @@ import { useErrorLogStoreWithOut } from '/@/store/modules/errorLog';
 import { useI18n } from '/@/hooks/web/useI18n';
 import { joinTimestamp, formatRequestDate } from './helper';
 import { useUserStoreWithOut } from '/@/store/modules/user';
+import { AxiosRetry } from '/@/utils/http/axios/axiosRetry';
 
 const globSetting = useGlobSetting();
 const urlPrefix = globSetting.urlPrefix;
@@ -158,7 +159,7 @@ const transform: AxiosTransform = {
   /**
    * @description: 响应错误处理
    */
-  responseInterceptorsCatch: (error: any) => {
+  responseInterceptorsCatch: (axiosInstance: AxiosResponse, error: any) => {
     const { t } = useI18n();
     const errorLogStore = useErrorLogStoreWithOut();
     errorLogStore.addAjaxErrorInfo(error);
@@ -189,6 +190,14 @@ const transform: AxiosTransform = {
     }
 
     checkStatus(error?.response?.status, msg, errorMessageMode);
+
+    // 添加自动重试机制 保险起见 只针对GET请求
+    const retryRequest = new AxiosRetry();
+    const { isOpenRetry } = config.requestOptions.retryRequest;
+    config.method?.toUpperCase() === RequestEnum.GET &&
+      isOpenRetry &&
+      // @ts-ignore
+      retryRequest.retry(axiosInstance, error);
     return Promise.reject(error);
   },
 };
@@ -234,6 +243,11 @@ function createAxios(opt?: Partial<CreateAxiosOptions>) {
           ignoreCancelToken: true,
           // 是否携带token
           withToken: true,
+          retryRequest: {
+            isOpenRetry: true,
+            count: 5,
+            waitTime: 100,
+          },
         },
       },
       opt || {},

+ 23 - 0
src/views/demo/feat/request-demo/index.vue

@@ -0,0 +1,23 @@
+<template>
+  <div class="request-box">
+    <a-button @click="handleClick" color="primary"> 点击会重新发起请求5次 </a-button>
+    <p>打开浏览器的network面板,可以看到发出了六次请求</p>
+  </div>
+</template>
+<script lang="ts" setup>
+  import { testRetry } from '/@/api/sys/user';
+  // @ts-ignore
+  const handleClick = async () => {
+    await testRetry();
+  };
+</script>
+
+<style lang="less">
+  .request-box {
+    margin: 50px;
+  }
+
+  p {
+    margin-top: 10px;
+  }
+</style>

+ 7 - 0
types/axios.d.ts

@@ -23,8 +23,15 @@ export interface RequestOptions {
   ignoreCancelToken?: boolean;
   // Whether to send token in header
   withToken?: boolean;
+  // 请求重试机制
+  retryRequest?: RetryRequest;
 }
 
+export interface RetryRequest {
+  isOpenRetry: boolean;
+  count: number;
+  waitTime: number;
+}
 export interface Result<T = any> {
   code: number;
   type: 'success' | 'error' | 'warning';