From eaf6f161322bcbdbd19ae46fbe603f47ccf98d94 Mon Sep 17 00:00:00 2001 From: ray_wuhao <443547225@qq.com> Date: Tue, 20 Jun 2023 13:37:40 +0800 Subject: [PATCH] v3.3.6 --- CHANGELOG.md | 14 +++ src/axios/README.md | 71 +++++++++++++++ src/axios/helper/interceptor.ts | 114 ++++++++++++++++-------- src/axios/inject/requestInject.ts | 35 ++++++-- src/axios/inject/responseInject.ts | 47 ++++++++-- src/axios/instance.ts | 32 ++++--- src/axios/type.ts | 16 +++- src/layout/components/MenuTag/index.tsx | 22 ++++- 8 files changed, 289 insertions(+), 62 deletions(-) create mode 100644 src/axios/README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a97a4958..88af7502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # CHANGE LOG +## 3.3.6 + +### Feats + +- 重写 axios interceptor 方法。现在逻辑更加清晰,并且支持请求错误、响应错误处理。补充了两个工具函数 +- MenuTag 支持动态更新所在位置 +- 修复了鉴权方法的 bug +- 更新了 router permission 方法(路由守卫) +- 补充了一些模块文档 + +### 补充 + +> 文档拖欠太多了,我补不回来了,就。。。算了吧,我在每个关键模块补充了对应的 md 说明文档,凑合一下吧。真希望有一个好心人帮补充文档。 + ## 3.3.5 ### Feats diff --git a/src/axios/README.md b/src/axios/README.md new file mode 100644 index 00000000..9b5c590c --- /dev/null +++ b/src/axios/README.md @@ -0,0 +1,71 @@ +## 说明 + +> axios 包,全局的 axios 使用入口 +> `src/axios/instance.ts` 文件为 axios 实例文件,应该不可更改,如果需要有拦截器相关的操作,应该在 `src/axios/inject` 文件下按照需求在对应注册器中进行注册相关方法。该项目将 axios instance 与 axios interceptor 进行解耦,避免实例文件的臃肿。 + +## 工具函数 + +- BeforeFetchFunction +- FetchErrorFunction + +> 两个工具函数方便类型推导。 + +## 约束 + +### 拦截器添加 + +> 这里以请求拦截器添加为示例 + +```ts +/** + * + * 拦截器说明 + * + * 注册一个根据系统当前环境是否携带测试环境请求拦截器 + */ + +import { appendRequestHeaders } from '@/axios/helper/axiosCopilot' + +import type { + RequestInterceptorConfig, + BeforeFetchFunction, + FetchErrorFunction, +} from '@/axios/type' + +const injectRequestHeaderOfEnv: BeforeFetchFunction< + RequestInterceptorConfig +> = (ins, mode) => { + if (mode === 'development') { + appendRequestHeaders(ins, [ + { + key: 'Development-Mode', + value: 'development', + }, + ]) + } +} + +/** + * + * 在 setupRequestInterceptor 中注册 + */ + +export const setupRequestInterceptor = () => { + setImplement( + 'implementRequestInterceptorArray', + [injectRequestHeaderOfEnv], + 'ok', + ) +} + +/** 至此完成了请求拦截器的注册 */ +``` + +### 注册器 + +> 每个类型注册器都有两个方法,用于注册拦截器方法。都以 setupXXX 开头命名。注册器以队列形式管理拦截器方法,所以可能需要注意执行顺序。如果有新的拦截器方法,需要根据其使用场景在对应的注册器中注册(注册器第二个参数中注册)。 + +- 请求注册器: setupRequestInterceptor +- 请求错误注册器: setupRequestErrorInterceptor +- 响应注册器: setupResponseInterceptor +- 响应错误注册器: setupResponseErrorInterceptor diff --git a/src/axios/helper/interceptor.ts b/src/axios/helper/interceptor.ts index c9d0cf65..82539da5 100644 --- a/src/axios/helper/interceptor.ts +++ b/src/axios/helper/interceptor.ts @@ -16,7 +16,8 @@ * 请求拦截器、响应拦截器 * 暴露启动方法调用所有已注册方法 * - * 为什么要把 create 方法拆成两个, emmmmmm 我也不知道 + * 该拦截器仅适合放置公共的 axios 拦截器操作, 并且采用队列形式管理请求拦截器的注入 + * 所以在使用的时候, 需要按照约定格式进行参数传递 */ import RequestCanceler from '@/axios/helper/canceler' @@ -25,73 +26,116 @@ import { getAppEnvironment } from '@use-utils/hook' import type { RequestInterceptorConfig, ResponseInterceptorConfig, - ImplementKey, ImplementQueue, + ErrorImplementQueue, + FetchType, } from '@/axios/type' +/** 当前请求的实例 */ const axiosFetchInstance = { requestInstance: null as RequestInterceptorConfig | null, responseInstance: null as ResponseInterceptorConfig | null, } +const axiosFetchError = { + requestError: null as null | unknown, + responseError: null as null | unknown, +} +/** 请求队列(区分 reslove 与 reject 状态) */ const implement: ImplementQueue = { implementRequestInterceptorArray: [], implementResponseInterceptorArray: [], } +const errorImplement: ErrorImplementQueue = { + implementRequestInterceptorErrorArray: [], + implementResponseInterceptorErrorArray: [], +} +/** 取消器实例 */ export const axiosCanceler = new RequestCanceler() export const useAxiosInterceptor = () => { - const getImplementKey = (key: ImplementKey) => { - return key === 'requestInstance' - ? 'implementRequestInterceptorArray' - : 'implementResponseInterceptorArray' + /** 创建拦截器实例 */ + const createAxiosInstance = ( + instance: RequestInterceptorConfig | ResponseInterceptorConfig, + instanceKey: keyof typeof axiosFetchInstance, + ) => { + instanceKey === 'requestInstance' + ? (axiosFetchInstance['requestInstance'] = + instance as RequestInterceptorConfig) + : (axiosFetchInstance['responseInstance'] = + instance as ResponseInterceptorConfig) } - const createRequestAxiosInstance = (instance: RequestInterceptorConfig) => { - axiosFetchInstance['requestInstance'] = instance + /** 获取当前实例 */ + const getAxiosInstance = (instanceKey: keyof typeof axiosFetchInstance) => { + return axiosFetchInstance[instanceKey] } - const createResponseAxiosInstance = (instance: ResponseInterceptorConfig) => { - axiosFetchInstance['responseInstance'] = instance + /** 设置注入方法队列 */ + const setImplement = ( + key: keyof ImplementQueue | keyof ErrorImplementQueue, + func: AnyFunc[], + fetchType: FetchType, + ) => { + fetchType === 'ok' ? (implement[key] = func) : (errorImplement[key] = func) } - /** 获取请求实例或者响应实例 */ - const getAxiosFetchInstance = (key: ImplementKey) => { - return axiosFetchInstance[key] + /** 获取队列中所有的所有拦截器方法 */ + const getImplement = ( + key: keyof ImplementQueue | keyof ErrorImplementQueue, + fetchType: FetchType, + ): AnyFunc[] => { + return fetchType === 'ok' ? implement[key] : errorImplement[key] } - /** 请求前, 执行队列所有方法 */ - const beforeAxiosFetch = (key: ImplementKey) => { - const funcArr = implement[getImplementKey(key)] - const instance = getAxiosFetchInstance(key) - const { MODE } = getAppEnvironment() - - if (instance) { - funcArr?.forEach((curr) => { + /** 队列执行器 */ + const implementer = (funcs: AnyFunc[], ...args: any[]) => { + if (Array.isArray(funcs)) { + funcs?.forEach((curr) => { if (typeof curr === 'function') { - curr(instance, MODE) + curr(...args) } }) } } - /** 设置拦截器队列 */ - const setImplementQueue = (func: AnyFunc[], key: ImplementKey) => { - if (func && key) { - implement[getImplementKey(key)] = func + /** 请求、响应前执行拦截器队列中的所有方法 */ + const beforeFetch = ( + key: keyof typeof axiosFetchInstance, + implementKey: keyof ImplementQueue | keyof ErrorImplementQueue, + fetchType: FetchType, + ) => { + const funcArr = + fetchType === 'ok' + ? implement[implementKey] + : errorImplement[implementKey] + const instance = getAxiosInstance(key) + const { MODE } = getAppEnvironment() + + if (instance) { + implementer(funcArr, instance, MODE) } } - /** 获取拦截器队列 */ - const getImplementQueue = (key: ImplementKey) => { - return implement[getImplementKey(key)] + /** 请求、响应错误时执行队列中所有方法 */ + const fetchError = ( + key: keyof typeof axiosFetchError, + error: unknown, + errorImplementKey: keyof ErrorImplementQueue, + ) => { + axiosFetchError[key] = error + + const funcArr = errorImplement[errorImplementKey] + const { MODE } = getAppEnvironment() + + implementer(funcArr, error, MODE) } return { - createRequestAxiosInstance, - createResponseAxiosInstance, - beforeAxiosFetch, - setImplementQueue, - getImplementQueue, - getAxiosFetchInstance, + createAxiosInstance, + setImplement, + getImplement, + getAxiosInstance, + beforeFetch, + fetchError, } } diff --git a/src/axios/inject/requestInject.ts b/src/axios/inject/requestInject.ts index d41d2481..f789c224 100644 --- a/src/axios/inject/requestInject.ts +++ b/src/axios/inject/requestInject.ts @@ -14,6 +14,8 @@ * 请求拦截器入口 * 被注册方法执行时其实例能够保证获取到, 所以不需要做额外空判断 * 在内部执行方法中, 已经做了边界处理 + * + * 提供两个工具方法, 方便类型推导 */ import { useAxiosInterceptor, axiosCanceler } from '@/axios/helper/interceptor' @@ -21,8 +23,12 @@ import { appendRequestHeaders } from '@/axios/helper/axiosCopilot' import { APP_CATCH_KEY } from '@/appConfig/appConfig' import { getCache } from '@/utils/cache' -import type { RequestInterceptorConfig, ImplementFunction } from '@/axios/type' -const { setImplementQueue } = useAxiosInterceptor() +import type { + RequestInterceptorConfig, + BeforeFetchFunction, + FetchErrorFunction, +} from '@/axios/type' +const { setImplement } = useAxiosInterceptor() /** * @@ -46,7 +52,7 @@ const requestHeaderToken = (ins: RequestInterceptorConfig, mode: string) => { } /** 注入请求头信息 */ -const injectRequestHeaders: ImplementFunction = ( +const injectRequestHeaders: BeforeFetchFunction = ( ins, mode, ) => { @@ -60,7 +66,7 @@ const injectRequestHeaders: ImplementFunction = ( } /** 注入重复请求拦截器 */ -const injectCanceler: ImplementFunction = ( +const injectCanceler: BeforeFetchFunction = ( ins, mode, ) => { @@ -68,11 +74,28 @@ const injectCanceler: ImplementFunction = ( axiosCanceler.addPendingRequest(ins) // 把当前的请求信息添加到 pendingRequest 表中 } +const requestError: FetchErrorFunction = (error, mode) => { + console.log(error, mode) +} + /** * * 注册请求拦截器 - * 请注意执行顺序, setImplementQueue 方法按照注册顺序执行 + * 请注意执行顺序 */ export const setupRequestInterceptor = () => { - setImplementQueue([injectRequestHeaders, injectCanceler], 'requestInstance') + setImplement( + 'implementRequestInterceptorArray', + [injectRequestHeaders, injectCanceler], + 'ok', + ) +} + +/** + * + * 注册请求错误拦截器 + * 请注意执行顺序 + */ +export const setupRequestErrorInterceptor = () => { + setImplement('implementRequestInterceptorErrorArray', [requestError], 'error') } diff --git a/src/axios/inject/responseInject.ts b/src/axios/inject/responseInject.ts index ad8f6f1c..34eba431 100644 --- a/src/axios/inject/responseInject.ts +++ b/src/axios/inject/responseInject.ts @@ -14,27 +14,64 @@ * 响应拦截器入口 * 被注册方法执行时其实例能够保证获取到, 所以不需要做额外空判断 * 在内部执行方法中, 已经做了边界处理 + * + * 提供两个工具方法, 方便类型推导 */ import { useAxiosInterceptor, axiosCanceler } from '@/axios/helper/interceptor' -import type { ResponseInterceptorConfig, ImplementFunction } from '@/axios/type' +import type { + ResponseInterceptorConfig, + BeforeFetchFunction, + FetchErrorFunction, +} from '@/axios/type' -const { setImplementQueue } = useAxiosInterceptor() +const { setImplement } = useAxiosInterceptor() /** 响应成功后移除缓存请求 url */ -const injectResponseCanceler: ImplementFunction = ( +const injectResponseCanceler: BeforeFetchFunction = ( ins, mode, ) => { axiosCanceler.removePendingRequest(ins.config) } +/** + * + * @param error 错误信息 + * @param mode 当前环境 + * + * 你可以在响应错误的时候做一些什么 + * 这里不做具体演示 + * + * 方法执行时会有两个参数, 可以根据报错信息与环境定做一些处理 + */ +const responseError: FetchErrorFunction = (error, mode) => { + console.log(error, mode) +} + /** * * 注册响应拦截器 - * 请注意执行顺序, setImplementQueue 方法按照注册顺序执行 + * 请注意执行顺序 */ export const setupResponseInterceptor = () => { - setImplementQueue([injectResponseCanceler], 'responseInstance') + setImplement( + 'implementResponseInterceptorArray', + [injectResponseCanceler], + 'ok', + ) +} + +/** + * + * 注册响应错误拦截器 + * 请注意执行顺序 + */ +export const setupResponseErrorInterceptor = () => { + setImplement( + 'implementResponseInterceptorErrorArray', + [responseError], + 'error', + ) } diff --git a/src/axios/instance.ts b/src/axios/instance.ts index f4b32b60..7959a583 100644 --- a/src/axios/instance.ts +++ b/src/axios/instance.ts @@ -11,7 +11,7 @@ /** * - * 请求, 响应拦截器 + * 请求拦截器与响应拦截器 * 如果有需要拓展拦截器, 请在 inject 目录下参照示例方法继续拓展 * 该页面不做改动与配置 */ @@ -19,42 +19,50 @@ import axios from 'axios' import { AXIOS_CONFIG } from '@/appConfig/requestConfig' import { useAxiosInterceptor, axiosCanceler } from '@/axios/helper/interceptor' -import { setupRequestInterceptor } from '@/axios/inject/requestInject' -import { setupResponseInterceptor } from '@/axios/inject/responseInject' +import { + setupResponseInterceptor, + setupResponseErrorInterceptor, +} from '@/axios/inject/responseInject' +import { + setupRequestInterceptor, + setupRequestErrorInterceptor, +} from '@/axios/inject/requestInject' import type { AxiosInstanceExpand } from './type' const server: AxiosInstanceExpand = axios.create(AXIOS_CONFIG) -const { - createRequestAxiosInstance, - createResponseAxiosInstance, - beforeAxiosFetch, -} = useAxiosInterceptor() +const { createAxiosInstance, beforeFetch, fetchError } = useAxiosInterceptor() server.interceptors.request.use( (request) => { - createRequestAxiosInstance(request) + createAxiosInstance(request, 'requestInstance') setupRequestInterceptor() - beforeAxiosFetch('requestInstance') + beforeFetch('requestInstance', 'implementRequestInterceptorArray', 'ok') return request }, (error) => { + setupRequestErrorInterceptor() + fetchError('requestError', error, 'implementRequestInterceptorErrorArray') + return Promise.reject(error) }, ) server.interceptors.response.use( (response) => { - createResponseAxiosInstance(response) + createAxiosInstance(response, 'responseInstance') setupResponseInterceptor() - beforeAxiosFetch('responseInstance') + beforeFetch('responseInstance', 'implementResponseInterceptorArray', 'ok') const { data } = response return Promise.resolve(data) }, (error) => { + setupResponseErrorInterceptor() + fetchError('responseError', error, 'implementResponseInterceptorErrorArray') + axiosCanceler.removePendingRequest(error.config || {}) return Promise.reject(error) diff --git a/src/axios/type.ts b/src/axios/type.ts index 9305ead3..40db396a 100644 --- a/src/axios/type.ts +++ b/src/axios/type.ts @@ -83,13 +83,23 @@ export type RequestInterceptorConfig = InternalAxiosRequestConfig export type ResponseInterceptorConfig = AxiosResponse -export type ImplementKey = 'requestInstance' | 'responseInstance' - export interface ImplementQueue { implementRequestInterceptorArray: AnyFunc[] implementResponseInterceptorArray: AnyFunc[] } -export type ImplementFunction< +export interface ErrorImplementQueue { + implementRequestInterceptorErrorArray: AnyFunc[] + implementResponseInterceptorErrorArray: AnyFunc[] +} + +export type BeforeFetchFunction< T = RequestInterceptorConfig | ResponseInterceptorConfig, > = (ins: K, mode: string) => void + +export type FetchType = 'ok' | 'error' + +export type FetchErrorFunction = ( + error: K, + mode: string, +) => void diff --git a/src/layout/components/MenuTag/index.tsx b/src/layout/components/MenuTag/index.tsx index 592fa2f7..a70fbf3e 100644 --- a/src/layout/components/MenuTag/index.tsx +++ b/src/layout/components/MenuTag/index.tsx @@ -19,6 +19,8 @@ * * root path 标签不可被关闭, 所以不会显示关闭按钮 * 页面刷新后, 仅会保留刷新前激活 key 的 tag 标签 + * + * 注入 MENU_TAG_DATA 属性, 用于动态更新 MenuTag 标签所在的位置 */ import './index.scss' @@ -31,6 +33,7 @@ import { uuid } from '@/utils/hook' import { hasClass } from '@/utils/element' import { redirectRouterToDashboard } from '@/router/helper/routerCopilot' import { ROOT_ROUTE } from '@/appConfig/appConfig' +import { getElement } from '@use-utils/element' import type { MenuOption, ScrollbarInst } from 'naive-ui' @@ -205,6 +208,7 @@ const MenuTag = defineComponent({ y: 0, actionDropdownShow: false, }) + const MENU_TAG_DATA = 'menu_tag_data' /** * @@ -373,6 +377,19 @@ const MenuTag = defineComponent({ } } + /** 动态更新 menu tag 所在位置 */ + const positionMenuTag = () => { + nextTick().then(() => { + const tags = getElement(`attr:${MENU_TAG_DATA}="${menuKey.value}"`) + + if (tags?.length) { + const [menuTag] = tags + + menuTag.scrollIntoView?.() + } + }) + } + /** 如果有且只有一个标签页时, 禁止全部关闭操作 */ watch( () => modelMenuTagOptions.value, @@ -388,6 +405,8 @@ const MenuTag = defineComponent({ if (oldData?.length) { if (newData.length > oldData?.length) { updateScrollBarPosition() + } else if (newData.length === oldData?.length) { + positionMenuTag() } } }, @@ -423,6 +442,7 @@ const MenuTag = defineComponent({ setCurrentContentmenuIndex, menuTagMouseenter, menuTagMouseleave, + MENU_TAG_DATA, } }, render() { @@ -482,7 +502,7 @@ const MenuTag = defineComponent({ onContextmenu: this.handleContextMenu.bind(this, idx), onMouseenter: this.menuTagMouseenter.bind(this, curr), onMouseleave: this.menuTagMouseleave.bind(this, curr), - tag_data: curr.path, + [this.MENU_TAG_DATA]: curr.path, }} > {typeof curr.label === 'function'