Sfoglia il codice sorgente

Merge pull request #280 from qqlcx5/main

feat(useRequest):  请求取消功能,有效避免内存泄漏和不必要的资源消耗,尤其适用于长时间运行的请求或组件卸载场景。
菲鸽 7 mesi fa
parent
commit
ad83f8d9d2
4 ha cambiato i file con 61 aggiunte e 18 eliminazioni
  1. 18 4
      src/hooks/useRequest.ts
  2. 15 9
      src/http/http.ts
  3. 5 0
      src/http/types.ts
  4. 23 5
      src/pages/about/components/request.vue

+ 18 - 4
src/hooks/useRequest.ts

@@ -1,4 +1,5 @@
 import type { Ref } from 'vue'
 import type { Ref } from 'vue'
+import type { HttpRequestResult } from '@/http/types'
 import { ref } from 'vue'
 import { ref } from 'vue'
 
 
 interface IUseRequestOptions<T> {
 interface IUseRequestOptions<T> {
@@ -13,6 +14,7 @@ interface IUseRequestReturn<T, P = undefined> {
   error: Ref<boolean | Error>
   error: Ref<boolean | Error>
   data: Ref<T | undefined>
   data: Ref<T | undefined>
   run: (args?: P) => Promise<T | undefined>
   run: (args?: P) => Promise<T | undefined>
+  cancel: () => void
 }
 }
 
 
 /**
 /**
@@ -24,15 +26,19 @@ interface IUseRequestReturn<T, P = undefined> {
  * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
  * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
  */
  */
 export default function useRequest<T, P = undefined>(
 export default function useRequest<T, P = undefined>(
-  func: (args?: P) => Promise<T>,
+  func: (args?: P) => HttpRequestResult<T>,
   options: IUseRequestOptions<T> = { immediate: false },
   options: IUseRequestOptions<T> = { immediate: false },
 ): IUseRequestReturn<T, P> {
 ): IUseRequestReturn<T, P> {
   const loading = ref(false)
   const loading = ref(false)
-  const error = ref(false)
+  const error = ref<boolean | Error>(false)
   const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
   const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
+  let requestTask: UniApp.RequestTask | undefined
+
   const run = async (args?: P) => {
   const run = async (args?: P) => {
     loading.value = true
     loading.value = true
-    return func(args)
+    const { promise, requestTask: task } = func(args)
+    requestTask = task // Store the requestTask
+    return promise
       .then((res) => {
       .then((res) => {
         data.value = res
         data.value = res
         error.value = false
         error.value = false
@@ -47,8 +53,16 @@ export default function useRequest<T, P = undefined>(
       })
       })
   }
   }
 
 
+  const cancel = () => {
+    if (requestTask) {
+      requestTask.abort()
+      loading.value = false // Reset loading state on cancel
+      error.value = new Error('Request cancelled') // Set a specific error for cancellation
+    }
+  }
+
   if (options.immediate) {
   if (options.immediate) {
     (run as (args?: P) => Promise<T | undefined>)({} as P)
     (run as (args?: P) => Promise<T | undefined>)({} as P)
   }
   }
-  return { loading, error, data, run }
+  return { loading, error, data, run, cancel }
 }
 }

+ 15 - 9
src/http/http.ts

@@ -8,12 +8,12 @@ import { ResultEnum } from './tools/enum'
 
 
 // 刷新 token 状态管理
 // 刷新 token 状态管理
 let refreshing = false // 防止重复刷新 token 标识
 let refreshing = false // 防止重复刷新 token 标识
-let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
+let taskQueue: { resolve: (value: any) => void, reject: (reason?: any) => void, options: CustomRequestOptions }[] = [] as { resolve: (value: any) => void, reject: (reason?: any) => void, options: CustomRequestOptions }[] // 刷新 token 请求队列
 
 
 export function http<T>(options: CustomRequestOptions) {
 export function http<T>(options: CustomRequestOptions) {
-  // 1. 返回 Promise 对象
-  return new Promise<T>((resolve, reject) => {
-    uni.request({
+  let requestTask: UniApp.RequestTask | undefined
+  const promise = new Promise<T>((resolve, reject) => {
+    requestTask = uni.request({
       ...options,
       ...options,
       dataType: 'json',
       dataType: 'json',
       // #ifndef MP-WEIXIN
       // #ifndef MP-WEIXIN
@@ -44,9 +44,7 @@ export function http<T>(options: CustomRequestOptions) {
           const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
           const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
           // token 失效的,且有刷新 token 的,才放到请求队列里
           // token 失效的,且有刷新 token 的,才放到请求队列里
           if ((res.statusCode === 401 || resData.code === 401) && refreshToken) {
           if ((res.statusCode === 401 || resData.code === 401) && refreshToken) {
-            taskQueue.push(() => {
-              resolve(http<T>(options))
-            })
+            taskQueue.push({ resolve, reject, options })
           }
           }
           // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
           // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
           if ((res.statusCode === 401 || resData.code === 401) && refreshToken && !refreshing) {
           if ((res.statusCode === 401 || resData.code === 401) && refreshToken && !refreshing) {
@@ -65,7 +63,9 @@ export function http<T>(options: CustomRequestOptions) {
                 })
                 })
               })
               })
               // 将任务队列的所有任务重新请求
               // 将任务队列的所有任务重新请求
-              taskQueue.forEach(task => task())
+              taskQueue.forEach((task) => {
+                http<T>(task.options).promise.then(task.resolve, task.reject)
+              })
             }
             }
             catch (refreshErr) {
             catch (refreshErr) {
               console.error('刷新 token 失败:', refreshErr)
               console.error('刷新 token 失败:', refreshErr)
@@ -103,7 +103,12 @@ export function http<T>(options: CustomRequestOptions) {
         }
         }
       },
       },
       // 响应失败
       // 响应失败
-      fail(err) {
+      fail(err: UniApp.RequestSuccessCallbackResult | UniApp.GeneralCallbackResult) {
+        console.log(`🚀 - fail - err:`, err)
+        // 如果是请求取消,则不显示错误提示
+        if (err.errMsg === 'request:fail abort') {
+          return reject(new Error('Request cancelled'))
+        }
         uni.showToast({
         uni.showToast({
           icon: 'none',
           icon: 'none',
           title: '网络错误,换个网络试试',
           title: '网络错误,换个网络试试',
@@ -112,6 +117,7 @@ export function http<T>(options: CustomRequestOptions) {
       },
       },
     })
     })
   })
   })
+  return { promise, requestTask: requestTask! }
 }
 }
 
 
 /**
 /**

+ 5 - 0
src/http/types.ts

@@ -7,6 +7,11 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
   hideErrorToast?: boolean
   hideErrorToast?: boolean
 } & IUniUploadFileOptions // 添加uni.uploadFile参数类型
 } & IUniUploadFileOptions // 添加uni.uploadFile参数类型
 
 
+export interface HttpRequestResult<T> {
+  promise: Promise<T>
+  requestTask: UniApp.RequestTask
+}
+
 // 通用响应格式
 // 通用响应格式
 export interface IResponse<T = any> {
 export interface IResponse<T = any> {
   code: number | string
   code: number | string

+ 23 - 5
src/pages/about/components/request.vue

@@ -8,10 +8,14 @@ import { getFooAPI } from '@/api/foo'
 // }
 // }
 const initialData = undefined
 const initialData = undefined
 
 
-const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
+const { loading, error, data, run, cancel } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
   immediate: true,
   immediate: true,
   initialData,
   initialData,
 })
 })
+function reqFooAPI() {
+  run()
+  cancel()
+}
 
 
 function reset() {
 function reset() {
   data.value = initialData
   data.value = initialData
@@ -31,17 +35,31 @@ function reset() {
       <button type="primary" size="mini" class="w-160px" @click="run">
       <button type="primary" size="mini" class="w-160px" @click="run">
         发送请求
         发送请求
       </button>
       </button>
+      <button type="default" size="mini" class="ml-4 w-160px" @click="reqFooAPI">
+        发送请求立即取消
+      </button>
+      <button type="default" size="mini" class="ml-4 w-160px" :disabled="!loading" @click="cancel">
+        取消请求
+      </button>
     </view>
     </view>
     <view class="h-16">
     <view class="h-16">
       <view v-if="loading">
       <view v-if="loading">
         loading...
         loading...
       </view>
       </view>
       <block v-else>
       <block v-else>
-        <view class="text-xl">
-          请求数据如下
+        <view v-if="error instanceof Error" class="text-red leading-8">
+          错误: {{ error.message }}
+        </view>
+        <view v-else-if="error" class="text-red leading-8">
+          错误: 未知错误
         </view>
         </view>
-        <view class="text-green leading-8">
-          {{ JSON.stringify(data) }}
+        <view v-else>
+          <view class="text-xl">
+            请求数据如下
+          </view>
+          <view class="text-green leading-8">
+            {{ JSON.stringify(data) }}
+          </view>
         </view>
         </view>
       </block>
       </block>
     </view>
     </view>