feat: 双token

This commit is contained in:
2026-06-16 18:19:48 +08:00
parent 172c83ea73
commit cb9687e472
8 changed files with 221 additions and 119 deletions

View File

@@ -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)
}
// 跳过鉴权时不注入 saTokenAccess 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,