244 lines
6.5 KiB
TypeScript
244 lines
6.5 KiB
TypeScript
import axios, { type AxiosInstance, type AxiosRequestConfig, type CreateAxiosDefaults } from 'axios'
|
||
import { useAccountStore } from '@/stores'
|
||
import { eventBus, AppEvents } from '@/utils'
|
||
import type { ApiResponse, InternalRequestConfig, RequestErrorEvent } from './types'
|
||
|
||
/**
|
||
* 替换 URL 中的路径参数占位符
|
||
*
|
||
* @example
|
||
* resolvePathParams('/user/:id/posts', { id: 1 }) // → '/user/1/posts'
|
||
*/
|
||
function resolvePathParams(url: string, pathParams?: Record<string, string | number>): string {
|
||
if (!pathParams) return url
|
||
return url.replace(/:(\w+)/g, (_, key) => {
|
||
const value = pathParams[key]
|
||
if (value === undefined) {
|
||
throw new Error(`路径参数 "${key}" 缺失`)
|
||
}
|
||
return String(value)
|
||
})
|
||
}
|
||
|
||
/**
|
||
* 基于 axios 封装的网络请求客户端
|
||
*
|
||
* 不依赖任何 UI 库,错误通过事件总线发布,由外部订阅处理。
|
||
*
|
||
* @example
|
||
* ```ts
|
||
* // GET 请求
|
||
* const res = await requestClient.get<UserListResult>('/users', { page: 1 })
|
||
*
|
||
* // POST 请求
|
||
* const res = await requestClient.post<LoginResult>('/auth/login', { username, password })
|
||
* ```
|
||
*/
|
||
class RequestClient {
|
||
/** axios 实例 */
|
||
private instance: AxiosInstance
|
||
|
||
constructor(config?: CreateAxiosDefaults) {
|
||
this.instance = axios.create({
|
||
baseURL: '/api',
|
||
timeout: 15000,
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
...config
|
||
})
|
||
|
||
this.setupInterceptors()
|
||
}
|
||
|
||
/** 注册请求和响应拦截器 */
|
||
private setupInterceptors(): void {
|
||
// === 请求拦截器 ===
|
||
this.instance.interceptors.request.use(
|
||
(config) => {
|
||
const reqConfig = config as InternalRequestConfig
|
||
|
||
// 替换路径参数 :param → 实际值
|
||
if (reqConfig.pathParams && config.url) {
|
||
config.url = resolvePathParams(config.url, reqConfig.pathParams)
|
||
}
|
||
|
||
// 跳过鉴权时不注入 token
|
||
if (!reqConfig?.skipAuth) {
|
||
const accountStore = useAccountStore()
|
||
if (accountStore.token) {
|
||
config.headers.Authorization = `Bearer ${accountStore.token}`
|
||
}
|
||
}
|
||
|
||
return config
|
||
},
|
||
(error) => {
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
// === 响应拦截器 ===
|
||
// 拦截器内部将 axiosResponse 解包为 ApiResponse,
|
||
// 返回类型与 axios 类型签名不匹配,但公共方法已声明正确返回类型。
|
||
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 = {
|
||
type: 'business',
|
||
code: data.code,
|
||
message: data.message || '请求失败'
|
||
}
|
||
|
||
// 401 → 额外发布未授权事件,解耦路由
|
||
if (data.code === 401) {
|
||
eventBus.emit(AppEvents.UNAUTHORIZED)
|
||
}
|
||
|
||
// 发布请求错误事件,由 errorUI 模块订阅展示
|
||
eventBus.emit(AppEvents.REQUEST_ERROR, errorEvent)
|
||
return Promise.reject(data)
|
||
},
|
||
(error) => {
|
||
let errorEvent: RequestErrorEvent
|
||
|
||
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})`
|
||
}
|
||
}
|
||
|
||
eventBus.emit(AppEvents.REQUEST_ERROR, errorEvent)
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
}
|
||
|
||
/**
|
||
* 通用请求方法
|
||
*
|
||
* 适用于路径参数、混合参数等复杂场景。
|
||
*
|
||
* @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<T = unknown>(
|
||
url: string,
|
||
params?: Record<string, unknown>,
|
||
config?: InternalRequestConfig
|
||
): Promise<ApiResponse<T>> {
|
||
return this.instance.get(url, { params, ...config })
|
||
}
|
||
|
||
/**
|
||
* POST 请求
|
||
* @param url - 请求地址
|
||
* @param data - 请求体
|
||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||
*/
|
||
post<T = unknown>(
|
||
url: string,
|
||
data?: unknown,
|
||
config?: InternalRequestConfig
|
||
): Promise<ApiResponse<T>> {
|
||
return this.instance.post(url, data, config)
|
||
}
|
||
|
||
/**
|
||
* PUT 请求
|
||
* @param url - 请求地址
|
||
* @param data - 请求体
|
||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||
*/
|
||
put<T = unknown>(
|
||
url: string,
|
||
data?: unknown,
|
||
config?: InternalRequestConfig
|
||
): Promise<ApiResponse<T>> {
|
||
return this.instance.put(url, data, config)
|
||
}
|
||
|
||
/**
|
||
* DELETE 请求
|
||
* @param url - 请求地址
|
||
* @param config - 额外配置(pathParams、requestOptions 等)
|
||
*/
|
||
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<T = unknown>(
|
||
url: string,
|
||
data?: unknown,
|
||
config?: InternalRequestConfig
|
||
): Promise<ApiResponse<T>> {
|
||
return this.instance.patch(url, data, config)
|
||
}
|
||
}
|
||
|
||
/** 请求客户端单例 */
|
||
export const requestClient = new RequestClient()
|