فهرست منبع

Merge pull request #338 from GreatAuk/fix-auth-guard

fix: 如果路由拦截器和http响应拦截都符合条件,可能会多次跳转登录页面
菲鸽 6 ماه پیش
والد
کامیت
444bda7809
9فایلهای تغییر یافته به همراه228 افزوده شده و 16 حذف شده
  1. 2 2
      openapi-ts-request.config.ts
  2. 2 2
      src/http/alova.ts
  3. 3 3
      src/http/http.ts
  4. 3 0
      src/http/types.ts
  5. 6 5
      src/router/interceptor.ts
  6. 2 2
      src/service/info.ts
  7. 2 2
      src/service/listAll.ts
  8. 166 0
      src/utils/debounce.ts
  9. 42 0
      src/utils/toLoginPage.ts

+ 2 - 2
openapi-ts-request.config.ts

@@ -5,8 +5,8 @@ export default defineConfig([
     describe: 'unibest-openapi-test',
     schemaPath: 'https://ukw0y1.laf.run/unibest-opapi-test.json',
     serversPath: './src/service',
-    requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions } from '@/http/types';`,
-    requestOptionsType: 'CustomRequestOptions',
+    requestLibPath: `import request from '@/http/vue-query';\n import { CustomRequestOptions_ } from '@/http/types';`,
+    requestOptionsType: 'CustomRequestOptions_',
     isGenReactQuery: false,
     reactQueryMode: 'vue',
     isGenJavaScript: false,

+ 2 - 2
src/http/alova.ts

@@ -4,7 +4,7 @@ import AdapterUniapp from '@alova/adapter-uniapp'
 import { createAlova } from 'alova'
 import { createServerTokenAuthentication } from 'alova/client'
 import VueHook from 'alova/vue'
-import { LOGIN_PAGE } from '@/router/config'
+import { toLoginPage } from '@/utils/toLoginPage'
 import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
 
 // 配置动态Tag
@@ -31,7 +31,7 @@ const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthenticati
       }
       catch (error) {
         // 切换到登录页
-        await uni.reLaunch({ url: LOGIN_PAGE })
+        toLoginPage({ mode: 'reLaunch' })
         throw error
       }
     },

+ 3 - 3
src/http/http.ts

@@ -1,9 +1,9 @@
 import type { IDoubleTokenRes } from '@/api/types/login'
 import type { CustomRequestOptions, IResponse } from '@/http/types'
 import { nextTick } from 'vue'
-import { LOGIN_PAGE } from '@/router/config'
 import { useTokenStore } from '@/store/token'
 import { isDoubleTokenMode } from '@/utils'
+import { toLoginPage } from '@/utils/toLoginPage'
 import { ResultEnum } from './tools/enum'
 
 // 刷新 token 状态管理
@@ -32,7 +32,7 @@ export function http<T>(options: CustomRequestOptions) {
           if (!isDoubleTokenMode) {
             // 未启用双token策略,清理用户信息,跳转到登录页
             tokenStore.logout()
-            uni.navigateTo({ url: LOGIN_PAGE })
+            toLoginPage()
             return reject(res)
           }
 
@@ -80,7 +80,7 @@ export function http<T>(options: CustomRequestOptions) {
               await tokenStore.logout()
               // 跳转到登录页
               setTimeout(() => {
-                uni.navigateTo({ url: LOGIN_PAGE })
+                toLoginPage()
               }, 2000)
             }
             finally {

+ 3 - 0
src/http/types.ts

@@ -7,6 +7,9 @@ export type CustomRequestOptions = UniApp.RequestOptions & {
   hideErrorToast?: boolean
 } & IUniUploadFileOptions // 添加uni.uploadFile参数类型
 
+/** 主要提供给 openapi-ts-request 生成的代码使用 */
+export type CustomRequestOptions_ = Omit<CustomRequestOptions, 'url'>
+
 export interface HttpRequestResult<T> {
   promise: Promise<T>
   requestTask: UniApp.RequestTask

+ 6 - 5
src/router/interceptor.ts

@@ -7,6 +7,7 @@ import { isMp } from '@uni-helper/uni-env'
 import { useTokenStore } from '@/store/token'
 import { isPageTabbar, tabbarStore } from '@/tabbar/store'
 import { getAllPages, getLastPage, HOME_PAGE, parseUrlToObj } from '@/utils/index'
+import { toLoginPage } from '@/utils/toLoginPage'
 import { EXCLUDE_LOGIN_PATH_LIST, isNeedLoginMode, LOGIN_PAGE, LOGIN_PAGE_ENABLE_IN_MP, NOT_FOUND_PAGE } from './config'
 
 export const FG_LOG_ENABLE = false
@@ -83,7 +84,7 @@ export const navigateToInterceptor = {
     if (Object.keys(myQuery).length) {
       fullPath += `?${Object.keys(myQuery).map(key => `${key}=${myQuery[key]}`).join('&')}`
     }
-    const redirectUrl = `${LOGIN_PAGE}?redirect=${encodeURIComponent(fullPath)}`
+    const redirectQuery = `?redirect=${encodeURIComponent(fullPath)}`
 
     // #region 1/2 默认需要登录的情况(白名单策略) ---------------------------
     if (isNeedLoginMode) {
@@ -96,8 +97,8 @@ export const navigateToInterceptor = {
         if (path === LOGIN_PAGE) {
           return true // 明确表示允许路由继续执行
         }
-        FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) redirectUrl:', redirectUrl)
-        uni.navigateTo({ url: redirectUrl })
+        FG_LOG_ENABLE && console.log('1 isNeedLogin(白名单策略) url:', fullPath)
+        toLoginPage({ queryString: redirectQuery })
         return false // 明确表示阻止原路由继续执行
       }
     }
@@ -107,8 +108,8 @@ export const navigateToInterceptor = {
     else {
       // 不需要登录里面的 EXCLUDE_LOGIN_PATH_LIST 表示黑名单,需要重定向到登录页
       if (judgeIsExcludePath(path)) {
-        FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) redirectUrl:', redirectUrl)
-        uni.navigateTo({ url: redirectUrl })
+        FG_LOG_ENABLE && console.log('2 isNeedLogin(黑名单策略) url:', fullPath)
+        toLoginPage({ queryString: redirectQuery })
         return false // 修改为false,阻止原路由继续执行
       }
       return true // 明确表示允许路由继续执行

+ 2 - 2
src/service/info.ts

@@ -1,12 +1,12 @@
 /* eslint-disable */
 // @ts-ignore
 import request from '@/http/vue-query';
-import { CustomRequestOptions } from '@/http/types';
+import { CustomRequestOptions_ } from '@/http/types';
 
 import * as API from './types';
 
 /** 用户信息 GET /user/info */
-export function infoUsingGet({ options }: { options?: CustomRequestOptions }) {
+export function infoUsingGet({ options }: { options?: CustomRequestOptions_ }) {
   return request<API.InfoUsingGetResponse>('/user/info', {
     method: 'GET',
     ...(options || {}),

+ 2 - 2
src/service/listAll.ts

@@ -1,7 +1,7 @@
 /* eslint-disable */
 // @ts-ignore
 import request from '@/http/vue-query';
-import { CustomRequestOptions } from '@/http/types';
+import { CustomRequestOptions_ } from '@/http/types';
 
 import * as API from './types';
 
@@ -9,7 +9,7 @@ import * as API from './types';
 export function listAllUsingGet({
   options,
 }: {
-  options?: CustomRequestOptions;
+  options?: CustomRequestOptions_;
 }) {
   return request<API.ListAllUsingGetResponse>('/user/listAll', {
     method: 'GET',

+ 166 - 0
src/utils/debounce.ts

@@ -0,0 +1,166 @@
+// fork from https://github.com/toss/es-toolkit/blob/main/src/function/debounce.ts
+// 文档可查看:https://es-toolkit.dev/reference/function/debounce.html
+// 如需要 throttle 功能,可 copy https://github.com/toss/es-toolkit/blob/main/src/function/throttle.ts
+
+interface DebounceOptions {
+  /**
+   * An optional AbortSignal to cancel the debounced function.
+   */
+  signal?: AbortSignal
+
+  /**
+   * An optional array specifying whether the function should be invoked on the leading edge, trailing edge, or both.
+   * If `edges` includes "leading", the function will be invoked at the start of the delay period.
+   * If `edges` includes "trailing", the function will be invoked at the end of the delay period.
+   * If both "leading" and "trailing" are included, the function will be invoked at both the start and end of the delay period.
+   * @default ["trailing"]
+   */
+  edges?: Array<'leading' | 'trailing'>
+}
+
+export interface DebouncedFunction<F extends (...args: any[]) => void> {
+  (...args: Parameters<F>): void
+
+  /**
+   * Schedules the execution of the debounced function after the specified debounce delay.
+   * This method resets any existing timer, ensuring that the function is only invoked
+   * after the delay has elapsed since the last call to the debounced function.
+   * It is typically called internally whenever the debounced function is invoked.
+   *
+   * @returns {void}
+   */
+  schedule: () => void
+
+  /**
+   * Cancels any pending execution of the debounced function.
+   * This method clears the active timer and resets any stored context or arguments.
+   */
+  cancel: () => void
+
+  /**
+   * Immediately invokes the debounced function if there is a pending execution.
+   * This method executes the function right away if there is a pending execution.
+   */
+  flush: () => void
+}
+
+/**
+ * Creates a debounced function that delays invoking the provided function until after `debounceMs` milliseconds
+ * have elapsed since the last time the debounced function was invoked. The debounced function also has a `cancel`
+ * method to cancel any pending execution.
+ *
+ * @template F - The type of function.
+ * @param {F} func - The function to debounce.
+ * @param {number} debounceMs - The number of milliseconds to delay.
+ * @param {DebounceOptions} options - The options object
+ * @param {AbortSignal} options.signal - An optional AbortSignal to cancel the debounced function.
+ * @returns A new debounced function with a `cancel` method.
+ *
+ * @example
+ * const debouncedFunction = debounce(() => {
+ *   console.log('Function executed');
+ * }, 1000);
+ *
+ * // Will log 'Function executed' after 1 second if not called again in that time
+ * debouncedFunction();
+ *
+ * // Will not log anything as the previous call is canceled
+ * debouncedFunction.cancel();
+ *
+ * // With AbortSignal
+ * const controller = new AbortController();
+ * const signal = controller.signal;
+ * const debouncedWithSignal = debounce(() => {
+ *  console.log('Function executed');
+ * }, 1000, { signal });
+ *
+ * debouncedWithSignal();
+ *
+ * // Will cancel the debounced function call
+ * controller.abort();
+ */
+export function debounce<F extends (...args: any[]) => void>(
+  func: F,
+  debounceMs: number,
+  { signal, edges }: DebounceOptions = {},
+): DebouncedFunction<F> {
+  let pendingThis: any
+  let pendingArgs: Parameters<F> | null = null
+
+  const leading = edges != null && edges.includes('leading')
+  const trailing = edges == null || edges.includes('trailing')
+
+  const invoke = () => {
+    if (pendingArgs !== null) {
+      func.apply(pendingThis, pendingArgs)
+      pendingThis = undefined
+      pendingArgs = null
+    }
+  }
+
+  const onTimerEnd = () => {
+    if (trailing) {
+      invoke()
+    }
+
+    // eslint-disable-next-line ts/no-use-before-define
+    cancel()
+  }
+
+  let timeoutId: ReturnType<typeof setTimeout> | null = null
+
+  const schedule = () => {
+    if (timeoutId != null) {
+      clearTimeout(timeoutId)
+    }
+
+    timeoutId = setTimeout(() => {
+      timeoutId = null
+
+      onTimerEnd()
+    }, debounceMs)
+  }
+
+  const cancelTimer = () => {
+    if (timeoutId !== null) {
+      clearTimeout(timeoutId)
+      timeoutId = null
+    }
+  }
+
+  const cancel = () => {
+    cancelTimer()
+    pendingThis = undefined
+    pendingArgs = null
+  }
+
+  const flush = () => {
+    invoke()
+  }
+
+  const debounced = function (this: any, ...args: Parameters<F>) {
+    if (signal?.aborted) {
+      return
+    }
+
+    // eslint-disable-next-line ts/no-this-alias
+    pendingThis = this
+    pendingArgs = args
+
+    const isFirstCall = timeoutId == null
+
+    schedule()
+
+    if (leading && isFirstCall) {
+      invoke()
+    }
+  }
+
+  debounced.schedule = schedule
+  debounced.cancel = cancel
+  debounced.flush = flush
+
+  signal?.addEventListener('abort', cancel, { once: true })
+
+  return debounced
+}

+ 42 - 0
src/utils/toLoginPage.ts

@@ -0,0 +1,42 @@
+import { LOGIN_PAGE } from '@/router/config'
+import { getLastPage } from '@/utils'
+import { debounce } from '@/utils/debounce'
+
+interface ToLoginPageOptions {
+  /**
+   * 跳转模式, uni.navigateTo | uni.reLaunch
+   * @default 'navigateTo'
+   */
+  mode?: 'navigateTo' | 'reLaunch'
+  /**
+   * 查询参数
+   * @example '?redirect=/pages/home/index'
+   */
+  queryString?: string
+}
+
+/**
+ * 跳转到登录页, 带防抖处理
+ *
+ * 如果要立即跳转,不做延时,可以使用 `toLoginPage.flush()` 方法
+ */
+export const toLoginPage = debounce((options: ToLoginPageOptions = {}) => {
+  const { mode = 'navigateTo', queryString = '' } = options
+
+  const url = `${LOGIN_PAGE}${queryString}`
+
+  // 获取当前页面路径
+  const currentPage = getLastPage()
+  const currentPath = `/${currentPage.route}`
+  // 如果已经在登录页,则不跳转
+  if (currentPath === LOGIN_PAGE) {
+    return
+  }
+
+  if (mode === 'navigateTo') {
+    uni.navigateTo({ url })
+  }
+  else {
+    uni.reLaunch({ url })
+  }
+}, 500)