feat: 双token
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig, type CreateAxiosDefaults } from 'axios'
|
||||
import axios, { type AxiosInstance, type CreateAxiosDefaults } from 'axios'
|
||||
import { useAccountStore } from '@/stores'
|
||||
import { eventBus, AppEvents } from '@/utils'
|
||||
import type { ApiResponse, InternalRequestConfig, RequestErrorEvent } from './types'
|
||||
import { createRefreshTokenInterceptor } from './refreshInterceptor'
|
||||
|
||||
/**
|
||||
* 替换 URL 中的路径参数占位符
|
||||
@@ -38,6 +39,12 @@ class RequestClient {
|
||||
/** axios 实例 */
|
||||
private instance: AxiosInstance
|
||||
|
||||
/** 是否正在刷新 token */
|
||||
isRefreshing = false
|
||||
|
||||
/** 刷新期间等待队列(401 请求在刷新完成前暂存,刷新后用新 token 重试) */
|
||||
refreshTokenQueue: Array<(token: string) => void> = []
|
||||
|
||||
constructor(config?: CreateAxiosDefaults) {
|
||||
this.instance = axios.create({
|
||||
baseURL: '/api',
|
||||
@@ -63,7 +70,7 @@ class RequestClient {
|
||||
config.url = resolvePathParams(config.url, reqConfig.pathParams)
|
||||
}
|
||||
|
||||
// 跳过鉴权时不注入 saToken(Access Token)
|
||||
// 跳过鉴权时不注入 saToken
|
||||
if (!reqConfig?.skipAuth) {
|
||||
const accountStore = useAccountStore()
|
||||
if (accountStore.accessToken) {
|
||||
@@ -78,107 +85,85 @@ class RequestClient {
|
||||
}
|
||||
)
|
||||
|
||||
// === 响应拦截器 ===
|
||||
// 拦截器内部将 axiosResponse 解包为 ApiResponse,
|
||||
// 返回类型与 axios 类型签名不匹配,但公共方法已声明正确返回类型。
|
||||
// === 通用响应拦截器 ===
|
||||
// 职责:解包 ApiResponse + 发布非 401 的错误事件。
|
||||
// 401 全部透传,由 refreshInterceptor 统一处理。
|
||||
this.instance.interceptors.response.use(
|
||||
// @ts-expect-error 拦截器解包 axiosResponse → ApiResponse
|
||||
(response) => {
|
||||
const data = response.data as ApiResponse
|
||||
|
||||
// 业务成功
|
||||
if (data.code === 0) {
|
||||
return data
|
||||
}
|
||||
|
||||
// 业务错误事件载荷
|
||||
const errorEvent: RequestErrorEvent = {
|
||||
// 401 透传给刷新拦截器,此处不处理
|
||||
if (data.code === 401) {
|
||||
return response
|
||||
}
|
||||
|
||||
eventBus.emit(AppEvents.REQUEST_ERROR, {
|
||||
type: 'business',
|
||||
code: data.code,
|
||||
message: data.message || '请求失败'
|
||||
}
|
||||
|
||||
// 401 → 额外发布未授权事件,解耦路由
|
||||
if (data.code === 401) {
|
||||
eventBus.emit(AppEvents.UNAUTHORIZED)
|
||||
}
|
||||
|
||||
// 发布请求错误事件,由 errorUI 模块订阅展示
|
||||
eventBus.emit(AppEvents.REQUEST_ERROR, errorEvent)
|
||||
} satisfies RequestErrorEvent)
|
||||
return Promise.reject(data)
|
||||
},
|
||||
(error) => {
|
||||
let errorEvent: RequestErrorEvent
|
||||
|
||||
// HTTP 401 透传给刷新拦截器
|
||||
if (error.response?.status === 401) {
|
||||
eventBus.emit(AppEvents.UNAUTHORIZED)
|
||||
errorEvent = {
|
||||
type: 'http',
|
||||
code: 401,
|
||||
message: '登录已过期,请重新登录'
|
||||
}
|
||||
} else if (!error.response) {
|
||||
errorEvent = {
|
||||
type: 'network',
|
||||
message: '网络异常,请检查网络连接'
|
||||
}
|
||||
} else {
|
||||
errorEvent = {
|
||||
type: 'http',
|
||||
code: error.response.status,
|
||||
message: `请求错误 (${error.response.status})`
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
eventBus.emit(AppEvents.REQUEST_ERROR, errorEvent)
|
||||
eventBus.emit(AppEvents.REQUEST_ERROR, {
|
||||
type: error.response ? 'http' : 'network',
|
||||
code: error.response?.status,
|
||||
message: error.response
|
||||
? `请求错误 (${error.response.status})`
|
||||
: '网络异常,请检查网络连接'
|
||||
} satisfies RequestErrorEvent)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// === Token 刷新拦截器 ===
|
||||
// 通用拦截器透传 401 → 此处统一处理刷新/重试/重新认证
|
||||
const { onFulfilled, onRejected } = createRefreshTokenInterceptor({
|
||||
client: this.instance,
|
||||
doReAuthenticate: async () => {
|
||||
const accountStore = useAccountStore()
|
||||
accountStore.logout()
|
||||
eventBus.emit(AppEvents.UNAUTHORIZED)
|
||||
},
|
||||
doRefreshToken: () => {
|
||||
const accountStore = useAccountStore()
|
||||
return accountStore.refreshAccessToken()
|
||||
},
|
||||
formatToken: (token: string) => token,
|
||||
isRefreshing: {
|
||||
get value() {
|
||||
return requestClient.isRefreshing
|
||||
},
|
||||
set value(v: boolean) {
|
||||
requestClient.isRefreshing = v
|
||||
}
|
||||
},
|
||||
refreshTokenQueue: this.refreshTokenQueue
|
||||
})
|
||||
// @ts-expect-error 拦截器解包 axiosResponse → ApiResponse
|
||||
this.instance.interceptors.response.use(onFulfilled, onRejected)
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用请求方法
|
||||
*
|
||||
* 适用于路径参数、混合参数等复杂场景。
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 路径参数 + 查询参数
|
||||
* requestClient.request<PostList>({
|
||||
* method: 'GET',
|
||||
* url: '/user/:id/posts',
|
||||
* params: { page: 1 },
|
||||
* pathParams: { id: 1 }
|
||||
* })
|
||||
*
|
||||
* // 路径参数 + 请求体
|
||||
* requestClient.request<Result>({
|
||||
* method: 'POST',
|
||||
* url: '/user/:id/post',
|
||||
* data: body,
|
||||
* pathParams: { id: 1 }
|
||||
* })
|
||||
*
|
||||
* // 自定义请求头 + 跳过鉴权
|
||||
* requestClient.request<Result>({
|
||||
* method: 'POST',
|
||||
* url: '/public/upload',
|
||||
* data: formData,
|
||||
* headers: { 'Content-Type': 'multipart/form-data' },
|
||||
* requestOptions: { skipAuth: true }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
request<T = unknown>(config: InternalRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.instance.request(config)
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
* @param url - 请求地址
|
||||
* @param params - 查询参数
|
||||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||||
*/
|
||||
/** GET 请求 */
|
||||
get<T = unknown>(
|
||||
url: string,
|
||||
params?: Record<string, unknown>,
|
||||
@@ -187,12 +172,7 @@ class RequestClient {
|
||||
return this.instance.get(url, { params, ...config })
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
* @param url - 请求地址
|
||||
* @param data - 请求体
|
||||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||||
*/
|
||||
/** POST 请求 */
|
||||
post<T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
@@ -201,12 +181,7 @@ class RequestClient {
|
||||
return this.instance.post(url, data, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
* @param url - 请求地址
|
||||
* @param data - 请求体
|
||||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||||
*/
|
||||
/** PUT 请求 */
|
||||
put<T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
@@ -215,21 +190,12 @@ class RequestClient {
|
||||
return this.instance.put(url, data, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
* @param url - 请求地址
|
||||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||||
*/
|
||||
/** DELETE 请求 */
|
||||
delete<T = unknown>(url: string, config?: InternalRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.instance.delete(url, config)
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH 请求
|
||||
* @param url - 请求地址
|
||||
* @param data - 请求体
|
||||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||||
*/
|
||||
/** PATCH 请求 */
|
||||
patch<T = unknown>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
|
||||
Reference in New Issue
Block a user